Compare commits

...

28 Commits

Author SHA1 Message Date
shimoda.m
3d9a254b63 2025/1/27 PH1エンハンス 本番リリース
メンテナンス告知文削除
2025-01-24 04:04:55 +00:00
shimoda.m
ff5533b647 Merged PR 1010: 2025/1/27 本番リリース 2025-01-21 05:23:54 +00:00
shimoda.m
b3845187f6 Merged PR 1009: Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"
Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"

Reverted commit `b5293888`.

デプロイミスによる切り戻し
2025-01-21 04:47:21 +00:00
shimoda.m
019c818a19 Merged PR 1008: Revert "2025/1/27 PH1エンハンス 本番リリース
Revert "2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除"

Reverted commit `1137d826`.

デプロイミスによる切り戻し。
2025-01-21 04:46:02 +00:00
shimoda.m
1137d826ae 2025/1/27 PH1エンハンス 本番リリース
メンテナンス文言削除
2025-01-21 04:00:54 +00:00
shimoda.m
b529388871 Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース 2025-01-21 02:59:31 +00:00
shimoda.m
aef17893d9 Merged PR 1004: 2025/1/27 本番リリース メンテナンス文言追加
## 概要

2025/1/27の本番リリースに向けて、トップページにメンテナンス文言を追加

リテラルは以下で確認中。(日付を変えただけなので問題ない認識)
OMDS_IS-459 【リリース】エンハンスリリース(1/27)のスケジュールについて

## UIの変更内容
![image.png](https://dev.azure.com/ODMSCloud/6023ff7b-d41c-4fa7-9c6f-f576ba48c07c/_apis/git/repositories/302da463-a2d7-40f9-b2bb-6e8edf324fa9/pullRequests/1004/attachments/image.png)
2025-01-21 01:52:47 +00:00
nik.n
4f598b0017 Merged PR 988: pipelineエラー解消のためglobal npmアップデート削除
### 概要
Dockerfileから `RUN npm install -g npm` 削除。Node.js(v18.17.1)とnpm(11.0.0)のバージョン互換性の問題により、pipeline が失敗していたためです。
現在のNode.jsイメージに含まれている npmバージョン(9.6.7)でプロジェクトの要件は満たされているため、グローバルnpmのアップグレードは不要と判断した

### 参考リンク
失敗した Pipeline ビルド
- [#1931 • Merged PR 986: 保守対応の内容反映](https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_build/results?buildId=1931&view=results)
2025-01-08 04:36:06 +00:00
SAITO-PC-3\saito.k
b71ec627d7 特別な文字列をエスケープしてからreplaceAllするように修正 2024-12-11 14:07:39 +09:00
SAITO-PC-3\saito.k
af56f8ccad Supportページにリンク追加。
画面デザイン修正
2024-12-04 10:07:12 +09:00
SAITO-PC-3\saito.k
0b451ed62f NotificationHub登録方法修正 2024-11-27 11:16:46 +09:00
saito.k
11395279af Merged PR 954: NotificationHubへのデバイス登録方法を修正
## 概要
[Task4580: NotificationHubへのデバイス登録方法を修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4580)

- NotificationHubのデバイス登録時に付与するInstallationIDをuuidからuserIdに変更

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 修正範囲がNotificationHubのserviceに閉じているため、既存のテストが通ることを確認したのみ

## 補足
- 相談、参考資料などがあれば
2024-11-26 08:45:27 +00:00
x.itou.t
a07cfe51aa Merged PR 948: PH1エンハンス先行リリース対応
## 概要
[ユーザー ストーリー 4489: 【PH1エンハンス】Dictation Finishedになったファイルのステータスを変更したい](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4489)
[ユーザー ストーリー 4491: 【PH1エンハンス】通知にユーザーIDを付加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation-2nd/_workitems/edit/4491)
2024-11-11 05:23:38 +00:00
saito.k
ad397f6fe7 Merged PR 933: Functions修正
## 概要
[Task4557: Functions修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4557)

- 国情報を追加
- テスト修正

## レビューポイント
- 特になし

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-10-16 07:40:27 +00:00
saito.k
85fdec2e5a Merged PR 922: Functions
## 概要
[Task4485: Functions](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4485)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-25 01:06:33 +00:00
saito.k
f1b75a7ff0 Merged PR 921: API修正
## 概要
[Task4478: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4478)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-18 01:35:28 +00:00
saito.k
6690302ac3 Merged PR 920: API修正
## 概要
[Task4336: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4336)

- 文字起こし完了時のメールを文字起こし担当のTypistに送信しないようにする

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- ほかのテストケースがすべて通ることを確認
- メール送信処理を確認するテストケースを追加

## 補足
- 相談、参考資料などがあれば
2024-08-09 07:47:33 +00:00
saito.k
1320222d79 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 06:13:40 +00:00
saito.k
baea8ce5e5 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:57:41 +00:00
saito.k
d69126a980 Update azure-pipelines-staging.yml for Azure Pipelines 2024-08-09 04:55:36 +00:00
SAITO-PC-3\saito.k
79a2b9f0a3 yml修正 2024-08-09 13:42:37 +09:00
SAITO-PC-3\saito.k
2a755f2bd3 使用するVM修正 2024-08-09 12:27:25 +09:00
SAITO-PC-3\saito.k
edb8a79f94 docker-composeをダウンロードするymlに修正 2024-08-09 12:24:00 +09:00
SAITO-PC-3\saito.k
4a5136deee yml修正3 2024-08-09 11:47:36 +09:00
SAITO-PC-3\saito.k
f2eaba7e5f yml修正2 2024-08-09 11:39:51 +09:00
SAITO-PC-3\saito.k
3b576c6a47 パイプラインのYamlを修正 2024-08-09 11:36:06 +09:00
saito.k
557fc48d05 Merged PR 919: 画面修正
## 概要
[Task4331: 画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4331)

- サポートページにアプリダウンロードリンクを追加

## レビューポイント
- 特になし

## 動作確認状況
- ローカルで確認
- サポートページのみで他画面に影響なし

## 補足
- 相談、参考資料などがあれば
2024-08-09 01:23:21 +00:00
saito.k
353d5ad462 Merged PR 918: analysisLicenses修正
## 概要
[Task4313: analysisLicenses修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4313)

- ライセンス推移CSVの出力内容を修正
  - 移行ライセンスの利用状況も出力する
  - 出力する移行ライセンスの条件は以下
     - 第五階層
     - 割り当て済
     - 有効期限が当日以降
     - 年間ライセンス
- テスト修正
  - 移行ライセンスの取得・変換ロジックのテスト修正

## レビューポイント
- 特になし

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-07-23 03:35:55 +00:00
119 changed files with 14606 additions and 1266 deletions

1
.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,363 @@
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConnectionを作成しておくこと
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
trigger:
branches:
include:
- release-ph1-enhance
tags:
include:
- stage-*
jobs:
- job: initialize
displayName: Initialize
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
persistCredentials: true
- script: |
git fetch origin release-ph1-enhance:release-ph1-enhance
if git merge-base --is-ancestor $(Build.SourceVersion) release-ph1-enhance; then
echo "This commit is in the release-ph1-enhance branch."
else
echo "This commit is not in the release-ph1-enhance branch."
exit 1
fi
displayName: 'タグが付けられたCommitがrelease-ph1-enhanceブランチに存在するか確認'
- job: backend_test
dependsOn: initialize
condition: succeeded('initialize')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_server/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build
dependsOn: backend_test
condition: succeeded('backend_test')
displayName: Build And Push Backend Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_server
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileServerDictation.dockerfile
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
- job: frontend_build_staging
dependsOn: backend_build
condition: succeeded('backend_build')
displayName: Build Frontend Files(staging)
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:stg
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: frontend_build_production
dependsOn: frontend_build_staging
condition: succeeded('frontend_build_staging')
displayName: Build Frontend Files(production)
variables:
storageAccountName: saomdspipeline
environment: production
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_client
verbose: false
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: cd dictation_client && npm run build:prod
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: dictation_client/build
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
replaceExistingArchive: true
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob upload \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--type block \
--overwrite \
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
- job: function_test
dependsOn: frontend_build_production
condition: succeeded('frontend_build_production')
displayName: UnitTest
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Bash@3
displayName: Bash Script (Test)
inputs:
targetType: inline
workingDirectory: dictation_function/.devcontainer
script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci
docker-compose exec -T dictation_function sudo npm run test
- job: function_build
dependsOn: function_test
condition: succeeded('function_test')
displayName: Build And Push Function Image
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: Npm@1
displayName: npm ci
inputs:
command: ci
workingDir: dictation_function
verbose: false
- task: Docker@0
displayName: build
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
dockerFile: DockerfileFunctionDictation.dockerfile
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
buildArguments: |
BUILD_VERSION=$(Build.SourceVersion)
- task: Docker@0
displayName: push
inputs:
azureSubscriptionEndpoint: 'omds-service-connection-stg'
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
action: Push an image
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
- job: backend_deploy
dependsOn: function_build
condition: succeeded('function_build')
displayName: Backend Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'app-odms-dictation-stg'
deployToSlotOrASE: true
resourceGroupName: 'stg-application-rg'
slotName: 'staging'
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
- job: frontend_deploy
dependsOn: backend_deploy
condition: succeeded('backend_deploy')
displayName: Deploy Frontend Files
variables:
storageAccountName: saomdspipeline
environment: staging
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureCLI@2
inputs:
azureSubscription: 'omds-service-connection-stg'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az storage blob download \
--auth-mode login \
--account-name $(storageAccountName) \
--container-name $(environment) \
--name $(Build.SourceVersion).zip \
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
- task: Bash@3
displayName: Bash Script
inputs:
targetType: inline
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
- task: AzureStaticWebApp@0
displayName: 'Static Web App: '
inputs:
workingDirectory: '$(Build.SourcesDirectory)'
app_location: '/$(Build.SourceVersion)'
config_file_location: /dictation_client
skip_app_build: true
skip_api_build: true
is_static_export: false
verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: function_deploy
dependsOn: frontend_deploy
condition: succeeded('frontend_deploy')
displayName: Function Deploy
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureFunctionAppContainer@1
inputs:
azureSubscription: 'omds-service-connection-stg'
appName: 'func-odms-dictation-stg'
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
- job: smoke_test
dependsOn: function_deploy
condition: succeeded('function_deploy')
displayName: 'smoke test'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
# スモークテスト用にjobを確保
- job: swap_slot
dependsOn: smoke_test
condition: succeeded('smoke_test')
displayName: 'Swap Staging and Production'
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureAppServiceManage@0
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
inputs:
azureSubscription: 'omds-service-connection-stg'
action: 'Swap Slots'
WebAppName: 'app-odms-dictation-stg'
ResourceGroupName: 'stg-application-rg'
SourceSlot: 'staging'
SwapWithProduction: true
- job: migration
dependsOn: swap_slot
condition: succeeded('swap_slot')
displayName: DB migration
pool:
name: odms-deploy-pipeline
steps:
- checkout: self
clean: true
fetchDepth: 1
- task: AzureKeyVault@2
displayName: 'Azure Key Vault: kv-odms-secret-stg'
inputs:
ConnectedServiceName: 'omds-service-connection-stg'
KeyVaultName: kv-odms-secret-stg
- task: CmdLine@2
displayName: migration
inputs:
script: >2
# DB接続情報書き換え
sed -i -e "s/DB_NAME/$(db-name-ph1-enhance)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
sql-migrate --version
cat ./dictation_server/db/dbconfig.yml
# migration実行
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci

View File

@ -43,11 +43,14 @@ jobs:
targetType: inline targetType: inline
workingDirectory: dictation_server/.devcontainer workingDirectory: dictation_server/.devcontainer
script: | script: |
docker-compose -f pipeline-docker-compose.yml build sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
docker-compose -f pipeline-docker-compose.yml up -d sudo chmod +x /usr/local/bin/docker-compose
docker-compose exec -T dictation_server sudo npm ci docker-compose --version
docker-compose exec -T dictation_server sudo npm run migrate:up:test docker-compose -f pipeline-docker-compose.yml build
docker-compose exec -T dictation_server sudo npm run test docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_server sudo npm ci
docker-compose exec -T dictation_server sudo npm run migrate:up:test
docker-compose exec -T dictation_server sudo npm run test
- job: backend_build - job: backend_build
dependsOn: backend_test dependsOn: backend_test
condition: succeeded('backend_test') condition: succeeded('backend_test')
@ -186,6 +189,9 @@ jobs:
targetType: inline targetType: inline
workingDirectory: dictation_function/.devcontainer workingDirectory: dictation_function/.devcontainer
script: | script: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose -f pipeline-docker-compose.yml build docker-compose -f pipeline-docker-compose.yml build
docker-compose -f pipeline-docker-compose.yml up -d docker-compose -f pipeline-docker-compose.yml up -d
docker-compose exec -T dictation_function sudo npm ci docker-compose exec -T dictation_function sudo npm ci

View File

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

View File

@ -27,6 +27,7 @@ module.exports = {
rules: { rules: {
"react/jsx-uses-react": "off", "react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"react/function-component-definition": [ "react/function-component-definition": [
"error", "error",
{ {

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@ import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssue
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice"; import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice"; import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice"; 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 dictation from "features/dictation/dictationSlice";
import partner from "features/partner/partnerSlice"; import partner from "features/partner/partnerSlice";
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice"; import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
@ -35,6 +37,8 @@ export const store = configureStore({
licenseSummary, licenseSummary,
licenseOrderHistory, licenseOrderHistory,
partnerLicense, partnerLicense,
licenseTrialIssue,
searchPartners,
dictation, dictation,
partner, partner,
typistGroup, typistGroup,

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 42.949219 37.109375 L 33.898438 28.0625 L 33.007812 28.949219 L 31.476562 27.414062 C 33.183594 24.882812 34.046875 21.945312 34.042969 19.011719 C 34.046875 15.171875 32.578125 11.320312 29.644531 8.390625 C 26.722656 5.464844 22.867188 4 19.019531 4.003906 C 15.179688 4 11.324219 5.46875 8.398438 8.390625 C 5.46875 11.320312 4 15.167969 4.003906 19.011719 C 4 22.851562 5.46875 26.703125 8.394531 29.628906 C 11.328125 32.554688 15.179688 34.019531 19.023438 34.019531 C 21.957031 34.019531 24.902344 33.160156 27.433594 31.453125 L 28.96875 32.988281 L 28.082031 33.871094 L 37.136719 42.917969 C 37.847656 43.636719 38.796875 43.996094 39.738281 43.996094 C 40.671875 43.996094 41.621094 43.636719 42.335938 42.917969 L 42.949219 42.308594 C 43.664062 41.59375 44.027344 40.644531 44.027344 39.707031 C 44.027344 38.769531 43.664062 37.824219 42.949219 37.109375 Z M 19.023438 32.003906 C 15.6875 32.003906 12.359375 30.738281 9.824219 28.199219 C 7.285156 25.667969 6.019531 22.34375 6.019531 19.011719 C 6.019531 15.675781 7.289062 12.351562 9.824219 9.820312 C 12.359375 7.285156 15.6875 6.019531 19.019531 6.019531 C 22.355469 6.019531 25.683594 7.285156 28.21875 9.820312 C 30.757812 12.351562 32.023438 15.675781 32.027344 19.011719 C 32.023438 22.34375 30.757812 25.667969 28.222656 28.199219 C 25.683594 30.738281 22.355469 32.003906 19.023438 32.003906 Z M 28.78125 30.421875 C 29.074219 30.171875 29.367188 29.910156 29.648438 29.628906 C 29.929688 29.351562 30.191406 29.058594 30.445312 28.761719 L 31.820312 30.136719 L 30.15625 31.800781 Z M 41.523438 40.882812 L 40.910156 41.492188 C 40.582031 41.820312 40.164062 41.976562 39.734375 41.980469 C 39.308594 41.980469 38.890625 41.820312 38.5625 41.496094 L 30.9375 33.875 L 33.898438 30.917969 L 41.523438 38.535156 C 41.847656 38.863281 42.007812 39.28125 42.007812 39.707031 C 42.007812 40.136719 41.847656 40.554688 41.523438 40.882812 Z M 41.523438 40.882812 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 25.695312 12.34375 C 23.855469 10.507812 21.433594 9.585938 19.023438 9.585938 C 16.609375 9.585938 14.191406 10.507812 12.351562 12.34375 C 10.511719 14.179688 9.589844 16.601562 9.59375 19.011719 C 9.589844 21.421875 10.511719 23.84375 12.351562 25.675781 C 14.191406 27.511719 16.609375 28.433594 19.019531 28.433594 C 21.433594 28.433594 23.855469 27.511719 25.695312 25.675781 C 27.535156 23.839844 28.453125 21.421875 28.453125 19.011719 C 28.457031 16.601562 27.53125 14.183594 25.695312 12.34375 Z M 24.503906 24.488281 C 22.992188 25.996094 21.011719 26.753906 19.019531 26.75 C 17.03125 26.75 15.050781 25.996094 13.539062 24.488281 C 12.027344 22.976562 11.277344 21 11.277344 19.011719 C 11.277344 17.023438 12.027344 15.042969 13.539062 13.53125 C 15.054688 12.023438 17.03125 11.269531 19.023438 11.265625 C 21.011719 11.269531 22.992188 12.023438 24.503906 13.53125 C 26.015625 15.042969 26.769531 17.023438 26.769531 19.011719 C 26.769531 21 26.015625 22.976562 24.503906 24.488281 Z M 24.503906 24.488281 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

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

View File

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

View File

@ -35,6 +35,8 @@ export const listTasksAsync = createAsyncThunk<
filter?: string; filter?: string;
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
authorId?: string;
fileName?: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -43,7 +45,8 @@ export const listTasksAsync = createAsyncThunk<
}; };
} }
>("dictations/listTasksAsync", async (args, thunkApi) => { >("dictations/listTasksAsync", async (args, thunkApi) => {
const { limit, offset, filter, direction, paramName } = args; const { limit, offset, filter, direction, paramName, authorId, fileName } =
args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -60,6 +63,8 @@ export const listTasksAsync = createAsyncThunk<
filter, filter,
direction, direction,
paramName, paramName,
authorId,
fileName,
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
@ -80,6 +85,136 @@ export const listTasksAsync = createAsyncThunk<
} }
}); });
export const getTaskFiltersAsync = createAsyncThunk<
{
authorId?: string;
fileName?: string;
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/getTaskFiltersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
const usertaskfilter = await usersApi.getTaskFilter({
headers: { authorization: `Bearer ${accessToken}` },
});
const { authorId, fileName } = usertaskfilter.data;
return { authorId, fileName };
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateTaskFiltersAsync = createAsyncThunk<
{
/** empty */
},
{
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateTaskFiltersAsync", async (args, thunkApi) => {
const { filterConditionAuthorId, filterConditionFileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateSortColumnAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateSortColumnAsync", async (args, thunkApi) => {
const { direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
return await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getSortColumnAsync = createAsyncThunk< export const getSortColumnAsync = createAsyncThunk<
{ {
direction: DirectionType; direction: DirectionType;
@ -280,6 +415,8 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
filterConditionAuthorId: string;
filterConditionFileName: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -288,7 +425,13 @@ export const playbackAsync = createAsyncThunk<
}; };
} }
>("dictations/playbackAsync", async (args, thunkApi) => { >("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName } = args; const {
audioFileId,
direction,
paramName,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -305,6 +448,12 @@ export const playbackAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
); );
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await tasksApi.checkout(audioFileId, { await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
@ -387,6 +536,8 @@ export const cancelAsync = createAsyncThunk<
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
isTypist: boolean; isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -395,7 +546,14 @@ export const cancelAsync = createAsyncThunk<
}; };
} }
>("dictations/cancelAsync", async (args, thunkApi) => { >("dictations/cancelAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args; const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -406,15 +564,25 @@ export const cancelAsync = createAsyncThunk<
const tasksApi = new TasksApi(config); const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config); const usersApi = new UsersApi(config);
try { try {
// ユーザーがタイピストである場合に、ソート条件を保存する // ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
if (isTypist) { if (isTypist) {
await usersApi.updateSortCriteria( await usersApi.updateSortCriteria(
{ direction, paramName }, {
direction,
paramName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
); );
} }
await tasksApi.cancel(audioFileId, { await tasksApi.cancel(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
@ -450,6 +618,93 @@ export const cancelAsync = createAsyncThunk<
} }
}); });
export const reopenAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/reopenAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
isTypist,
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 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 },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.reopen(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Finished]以外、またはタスクが存在しない場合、またはtypistで自分のタスクでない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.reopenFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listBackupPopupTasksAsync = createAsyncThunk< export const listBackupPopupTasksAsync = createAsyncThunk<
TasksResponse, TasksResponse,
{ {
@ -480,6 +735,8 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.DESC, DIRECTION.DESC,
SORTABLE_COLUMN.Status, SORTABLE_COLUMN.Status,
undefined, // backupポップアップ表示時には検索条件は未指定
undefined, // backupポップアップ表示時には検索条件は未指定
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }

View File

@ -72,6 +72,12 @@ export const selectDirection = (state: RootState) =>
export const selectParamName = (state: RootState) => export const selectParamName = (state: RootState) =>
state.dictation.apps.paramName; 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) => export const selectSelectedTask = (state: RootState) =>
state.dictation.apps.selectedTask; state.dictation.apps.selectedTask;

View File

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

View File

@ -7,3 +7,8 @@ export const STATUS = {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
ORDER_CANCELED: "Order Canceled", ORDER_CANCELED: "Order Canceled",
} as const; } as const;
export const LICENSE_TYPE = {
NORMAL: "NORMAL",
TRIAL: "TRIAL",
} as const;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,60 @@
import { createSlice } from "@reduxjs/toolkit";
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
import { LicenseTrialIssueState } from "./state";
import { issueTrialLicenseAsync } from "./operations";
import {
TRIAL_LICENSE_EXPIRATION_DAY,
ISSUED_TRIAL_LICENSE_QUANTITY,
} from "./constants";
const initialState: LicenseTrialIssueState = {
apps: {
isLoading: false,
expirationDate: "",
quantity: ISSUED_TRIAL_LICENSE_QUANTITY,
},
};
export const licenseTrialIssueSlice = createSlice({
name: "licenseTrialIssue",
initialState,
reducers: {
cleanupApps: (state) => {
state.apps = initialState.apps;
},
setExpirationDate: (state) => {
// 有効期限を設定
const currentDate = new Date();
const expiryDate = new Date();
expiryDate.setDate(currentDate.getDate() + TRIAL_LICENSE_EXPIRATION_DAY);
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
const expirationDateLocal = convertLocalToUTCDate(expiryDate);
const expirationDateWithoutTime = new Date(
expirationDateLocal.getFullYear(),
expirationDateLocal.getMonth(),
expirationDateLocal.getDate()
);
const expirationYear = expirationDateWithoutTime.getFullYear();
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
const expirationDay = expirationDateWithoutTime.getDate();
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay} (${TRIAL_LICENSE_EXPIRATION_DAY})`;
state.apps.expirationDate = formattedExpirationDate;
},
},
extraReducers: (builder) => {
builder.addCase(issueTrialLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(issueTrialLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(issueTrialLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { cleanupApps, setExpirationDate } =
licenseTrialIssueSlice.actions;
export default licenseTrialIssueSlice.reducer;

View File

@ -0,0 +1,84 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import {
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const issueTrialLicenseAsync = createAsyncThunk<
{
/* Empty Object */
},
{
selectedRow?: PartnerLicenseInfo | SearchPartner;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/issueTrialLicenseAsync", async (args, thunkApi) => {
const { selectedRow } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
if (!selectedRow) {
// アカウントが選択されていない場合はエラーとする。
const errorMessage = getTranslationID(
"trialLicenseIssuePopupPage.message.accountNotSelected"
);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return {};
}
// トライアルライセンス発行処理を実行
await licensesApi.issueTrialLicenses(
{
issuedAccount: selectedRow.accountId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,10 @@
import { RootState } from "app/store";
export const selectIsLoading = (state: RootState) =>
state.licenseTrialIssue.apps.isLoading;
export const selectExpirationDate = (state: RootState) =>
state.licenseTrialIssue.apps.expirationDate;
export const selectNumberOfLicenses = (state: RootState) =>
state.licenseTrialIssue.apps.quantity;

View File

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

View File

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

View File

@ -30,3 +30,10 @@ export const selectCurrentPage = (state: RootState) => {
}; };
export const selectSelectedRow = (state: RootState) => export const selectSelectedRow = (state: RootState) =>
state.partnerLicense.apps.selectedRow; 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,6 +20,9 @@ export interface Apps {
hierarchicalElements: HierarchicalElement[]; hierarchicalElements: HierarchicalElement[];
isLoading: boolean; isLoading: boolean;
selectedRow?: PartnerLicenseInfo; selectedRow?: PartnerLicenseInfo;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
isSearchPopupOpen: boolean;
} }
export interface HierarchicalElement { export interface HierarchicalElement {

View File

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

View File

@ -0,0 +1,96 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { getAccessToken } from "features/auth";
import type { RootState } from "../../../app/store";
import { getTranslationID } from "../../../translation";
import { openSnackbar } from "../../ui/uiSlice";
import { AccountsApi, SearchPartner, PartnerHierarchy } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const searchPartnersAsync = createAsyncThunk<
// 正常時の戻り値の型
SearchPartner[],
// 引数
{
companyName?: string;
accountId?: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/searchPartners", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { companyName, accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const searchPartnerResponse = await accountsApi.searchPartners(
companyName,
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return searchPartnerResponse.data.searchResult;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getPartnerHierarchy = createAsyncThunk<
// 正常時の戻り値の型
PartnerHierarchy[],
// 引数
{
accountId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("licenses/getPartnerHierarchy", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { accountId } = args;
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const partnerHierarchyResponse = await accountsApi.getPartnerHierarchy(
accountId,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return partnerHierarchyResponse.data.accountHierarchy;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,80 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { SearchPartner } from "../../../api";
import { SearchPartnerState } from "./state";
import { searchPartnersAsync, getPartnerHierarchy } from "./operations";
const initialState: SearchPartnerState = {
domain: {
searchResult: [],
partnerHierarchy: [],
},
apps: {
isLoading: false,
selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
},
};
export const searchPartnersSlice = createSlice({
name: "searchPartners",
initialState,
reducers: {
changeSelectedRow: (
state,
action: PayloadAction<{ value?: SearchPartner }>
) => {
const { value } = action.payload;
state.apps.selectedRow = value;
},
setIsLicenseOrderHistoryOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
},
setIsViewDetailsOpen: (
state,
action: PayloadAction<{ value: boolean }>
) => {
state.apps.isViewDetailsOpen = action.payload.value;
},
cleanupSearchResult: (state) => {
state.domain.searchResult = initialState.domain.searchResult;
},
cleanupPartnerHierarchy: (state) => {
state.domain.partnerHierarchy = initialState.domain.partnerHierarchy;
},
},
extraReducers: (builder) => {
builder.addCase(searchPartnersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(searchPartnersAsync.fulfilled, (state, action) => {
state.domain.searchResult = action.payload;
state.apps.isLoading = false;
});
builder.addCase(searchPartnersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getPartnerHierarchy.fulfilled, (state, action) => {
state.domain.partnerHierarchy = action.payload;
state.apps.isLoading = false;
});
builder.addCase(getPartnerHierarchy.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
changeSelectedRow,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
cleanupSearchResult,
cleanupPartnerHierarchy,
} = searchPartnersSlice.actions;
export default searchPartnersSlice.reducer;

View File

@ -0,0 +1,14 @@
import { RootState } from "../../../app/store";
export const selectSearchResult = (state: RootState) =>
state.searchPartners.domain.searchResult;
export const selectPartnerHierarchy = (state: RootState) =>
state.searchPartners.domain.partnerHierarchy;
export const selectIsLoading = (state: RootState) =>
state.searchPartners.apps.isLoading;
export const selectSelectedRow = (state: RootState) =>
state.searchPartners.apps.selectedRow;
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
state.searchPartners.apps.isLicenseOrderHistoryOpen;
export const selectIsViewDetailsOpen = (state: RootState) =>
state.searchPartners.apps.isViewDetailsOpen;

View File

@ -0,0 +1,18 @@
import { SearchPartner, PartnerHierarchy } from "../../../api/api";
export interface SearchPartnerState {
domain: Domain;
apps: Apps;
}
export interface Domain {
searchResult: SearchPartner[];
partnerHierarchy: PartnerHierarchy[];
}
export interface Apps {
isLoading: boolean;
selectedRow?: SearchPartner;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
}

View File

@ -18,7 +18,7 @@ export const listUsersAsync = createAsyncThunk<
// 正常時の戻り値の型 // 正常時の戻り値の型
GetUsersResponse, GetUsersResponse,
// 引数 // 引数
void, undefined | { userInputUserName?: string; userInputEmail?: string },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
rejectValue: { rejectValue: {
@ -33,9 +33,11 @@ export const listUsersAsync = createAsyncThunk<
const accessToken = getAccessToken(state.auth); const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration); const config = new Configuration(configuration);
const usersApi = new UsersApi(config); const usersApi = new UsersApi(config);
const userInputUserName = args?.userInputUserName;
const userInputEmail = args?.userInputEmail;
try { try {
const res = await usersApi.getUsers({ const res = await usersApi.getUsers(userInputUserName, userInputEmail, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });
@ -500,6 +502,72 @@ export const deleteUserAsync = createAsyncThunk<
} }
}); });
export const confirmUserForceAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
{
userId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/confirmUserForceAsync", async (args, thunkApi) => {
const { userId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
await usersApi.confirmUserForce(
{
userId,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
// ユーザーが既に認証済みのため、強制認証不可
if (error.code === "E010202") {
errorMessage = getTranslationID(
"userListPage.message.alreadyEmailVerifiedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const importUsersAsync = createAsyncThunk< export const importUsersAsync = createAsyncThunk<
// 正常時の戻り値の型 // 正常時の戻り値の型
{ {

View File

@ -22,6 +22,8 @@ import {
selectDirection, selectDirection,
changeParamName, changeParamName,
changeDirection, changeDirection,
changeAuthorId,
changeFileName,
changeSelectedTask, changeSelectedTask,
openFilePropertyInfo, openFilePropertyInfo,
SortableColumnType, SortableColumnType,
@ -32,10 +34,16 @@ import {
selectIsLoading, selectIsLoading,
playbackAsync, playbackAsync,
cancelAsync, cancelAsync,
reopenAsync,
PRIORITY, PRIORITY,
deleteTaskAsync, deleteTaskAsync,
isSortableColumnType, isSortableColumnType,
isDirectionType, isDirectionType,
getTaskFiltersAsync,
selectAuthorId,
selectFilename,
updateTaskFiltersAsync,
updateSortColumnAsync,
} from "features/dictation"; } from "features/dictation";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import { Task } from "api/api"; import { Task } from "api/api";
@ -54,6 +62,7 @@ import { DisPlayInfo } from "./displayInfo";
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup"; import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
import { BackupPopup } from "./backupPopup"; import { BackupPopup } from "./backupPopup";
import { FilePropertyPopup } from "./filePropertyPopup"; import { FilePropertyPopup } from "./filePropertyPopup";
import searchIcon from "../../assets/images/search.svg";
const DictationPage: React.FC = (): JSX.Element => { const DictationPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
@ -100,10 +109,18 @@ const DictationPage: React.FC = (): JSX.Element => {
const [filterFinished, setFilterFinished] = useState(true); const [filterFinished, setFilterFinished] = useState(true);
const [filterBackup, setFilterBackup] = useState(false); const [filterBackup, setFilterBackup] = useState(false);
// 検索条件の入力値
const [filterConditionAuthorId, setFilterConditionAuthorId] = useState("");
const [filterConditionFileName, setFilterConditionFileName] = useState("");
// ソート対象カラム // ソート対象カラム
const sortableParamName = useSelector(selectParamName); const sortableParamName = useSelector(selectParamName);
const sortDirection = useSelector(selectDirection); const sortDirection = useSelector(selectDirection);
// task_filtersテーブルの検索条件
const authorId = useSelector(selectAuthorId);
const fileName = useSelector(selectFilename);
const tasks = useSelector(selectTasks); const tasks = useSelector(selectTasks);
const total = useSelector(selectTotal); const total = useSelector(selectTotal);
const totalPage = useSelector(selectTotalPage); const totalPage = useSelector(selectTotalPage);
@ -127,6 +144,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -140,6 +159,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
]); ]);
const getLastPage = useCallback(() => { const getLastPage = useCallback(() => {
@ -158,6 +179,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -172,6 +195,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
]); ]);
const getPrevPage = useCallback(() => { const getPrevPage = useCallback(() => {
@ -190,6 +215,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -204,6 +231,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
]); ]);
const getNextPage = useCallback(() => { const getNextPage = useCallback(() => {
@ -222,6 +251,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -236,6 +267,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
]); ]);
const updateSortColumn = useCallback( const updateSortColumn = useCallback(
@ -268,6 +301,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: currentDirection, direction: currentDirection,
paramName, paramName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -282,6 +317,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterPending, filterPending,
filterFinished, filterFinished,
filterBackup, filterBackup,
authorId,
fileName,
] ]
); );
@ -331,6 +368,19 @@ const DictationPage: React.FC = (): JSX.Element => {
hasFinished, hasFinished,
hasBackup hasBackup
); );
// フィルターの状態をローカルストレージに保存する
localStorage.setItem(
"filterCriteria",
JSON.stringify({
Uploaded: hasUploaded,
InProgress: hasInProgress,
Pending: hasPending,
Finished: hasFinished,
Backup: hasBackup,
})
);
dispatch( dispatch(
listTasksAsync({ listTasksAsync({
limit: LIMIT_TASK_NUM, limit: LIMIT_TASK_NUM,
@ -338,12 +388,14 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
dispatch(listTypistGroupsAsync()); dispatch(listTypistGroupsAsync());
}, },
[dispatch, sortDirection, sortableParamName] [dispatch, sortDirection, sortableParamName, authorId, fileName]
); );
const onPlayBack = useCallback( const onPlayBack = useCallback(
@ -359,6 +411,8 @@ const DictationPage: React.FC = (): JSX.Element => {
audioFileId, audioFileId,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
}) })
); );
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
@ -378,14 +432,15 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
dispatch(listTypistGroupsAsync()); dispatch(listTypistGroupsAsync());
const url = `${ const url = `${import.meta.env.VITE_DESK_TOP_APP_SCHEME
import.meta.env.VITE_DESK_TOP_APP_SCHEME }:playback?audioId=${audioFileId}`;
}:playback?audioId=${audioFileId}`;
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
document.body.appendChild(a); document.body.appendChild(a);
@ -402,6 +457,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterUploaded, filterUploaded,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
t, t,
] ]
); );
@ -423,6 +480,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
} }
@ -437,6 +496,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
] ]
); );
@ -454,6 +515,8 @@ const DictationPage: React.FC = (): JSX.Element => {
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
isTypist, isTypist,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
}) })
); );
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
@ -471,6 +534,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -487,6 +552,67 @@ const DictationPage: React.FC = (): JSX.Element => {
isTypist, isTypist,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
t,
]
);
const onReopen = useCallback(
async (audioFileId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(
reopenAsync({
audioFileId,
direction: sortDirection,
paramName: sortableParamName,
isTypist,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
})
);
if (meta.requestStatus === "fulfilled") {
const filter = getFilter(
filterUploaded,
filterInProgress,
filterPending,
filterFinished,
filterBackup
);
dispatch(
listTasksAsync({
limit: LIMIT_TASK_NUM,
offset: 0,
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
dispatch(listTypistGroupsAsync());
}
},
[
dispatch,
filterBackup,
filterFinished,
filterInProgress,
filterPending,
filterUploaded,
isTypist,
sortDirection,
sortableParamName,
authorId,
fileName,
t, t,
] ]
); );
@ -516,6 +642,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
} }
@ -530,9 +658,28 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup, filterBackup,
sortDirection, sortDirection,
sortableParamName, 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 = ( const sortIconClass = (
currentParam: SortableColumnType, currentParam: SortableColumnType,
currentDirection: DirectionType, currentDirection: DirectionType,
@ -575,6 +722,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
authorId,
fileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -590,10 +739,68 @@ const DictationPage: React.FC = (): JSX.Element => {
filterUploaded, filterUploaded,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
authorId,
fileName,
t, 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(() => { useEffect(() => {
(async () => { (async () => {
@ -609,21 +816,73 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDisplayInfo({ column: displayInfo })); dispatch(changeDisplayInfo({ column: displayInfo }));
const filter = getFilter(true, true, true, true, false); // フィルター状態をローカルストレージから取得する
const filterValue = localStorage.getItem("filterCriteria");
const { meta, payload } = await dispatch(getSortColumnAsync()); let filter: string | undefined;
if (filterValue) {
const parsedFilter = JSON.parse(filterValue);
setFilterUploaded(parsedFilter.Uploaded);
setFilterInProgress(parsedFilter.InProgress);
setFilterPending(parsedFilter.Pending);
setFilterFinished(parsedFilter.Finished);
setFilterBackup(parsedFilter.Backup);
filter = getFilter(
parsedFilter.Uploaded,
parsedFilter.InProgress,
parsedFilter.Pending,
parsedFilter.Finished,
parsedFilter.Backup
);
} else {
filter = getFilter(true, true, true, true, false);
localStorage.setItem(
"filterCriteria",
JSON.stringify({
Uploaded: true,
InProgress: true,
Pending: true,
Finished: true,
Backup: false,
})
);
}
// タスクフィルター条件
const { meta: taskFilterMeta, payload: taskfilterPayload } =
await dispatch(getTaskFiltersAsync());
let payloadAuthorId: string | undefined;
let payloadFileName: string | undefined;
if ( if (
meta.requestStatus === "fulfilled" && taskFilterMeta.requestStatus === "fulfilled" &&
payload && taskfilterPayload &&
!("error" in payload) !("error" in taskfilterPayload)
) {
payloadAuthorId = taskfilterPayload.authorId ?? "";
payloadFileName = taskfilterPayload.fileName ?? "";
dispatch(changeAuthorId({ authorId: payloadAuthorId }));
dispatch(changeFileName({ fileName: payloadFileName }));
// 初回表示時に検索フォームにtask_filtersテーブルの値を設定する。
setFilterConditionAuthorId(payloadAuthorId);
setFilterConditionFileName(payloadFileName);
}
// ソート条件
const { meta: sortCriteriaMeta, payload: sortCriteriaPayload } =
await dispatch(getSortColumnAsync());
let direction: DirectionType = "ASC";
let paramName: SortableColumnType = "JOB_NUMBER";
if (
sortCriteriaMeta.requestStatus === "fulfilled" &&
sortCriteriaPayload &&
!("error" in sortCriteriaPayload)
) { ) {
// ソート情報をローカルストレージから取得する // ソート情報をローカルストレージから取得する
const sortColumnValue = localStorage.getItem("sortCriteria") ?? ""; const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
let direction: DirectionType;
let paramName: SortableColumnType;
if (sortColumnValue === "") { if (sortColumnValue === "") {
direction = payload.direction; direction = sortCriteriaPayload.direction;
paramName = payload.paramName; paramName = sortCriteriaPayload.paramName;
} else { } else {
// ソート情報をDirectionとParamNameに分割する // ソート情報をDirectionとParamNameに分割する
const sortColumn = sortColumnValue?.split(","); const sortColumn = sortColumnValue?.split(",");
@ -634,15 +893,18 @@ const DictationPage: React.FC = (): JSX.Element => {
// 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する // 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
direction = isDirectionType(localStorageDirection) direction = isDirectionType(localStorageDirection)
? localStorageDirection ? localStorageDirection
: payload.direction; : sortCriteriaPayload.direction;
paramName = isSortableColumnType(localStorageParamName) paramName = isSortableColumnType(localStorageParamName)
? localStorageParamName ? localStorageParamName
: payload.paramName; : sortCriteriaPayload.paramName;
dispatch(changeDirection({ direction })); dispatch(changeDirection({ direction }));
dispatch(changeParamName({ paramName })); dispatch(changeParamName({ paramName }));
} }
}
// タスク一覧を取得する
if (isDirectionType(direction) && isSortableColumnType(paramName)) {
dispatch( dispatch(
listTasksAsync({ listTasksAsync({
limit: LIMIT_TASK_NUM, limit: LIMIT_TASK_NUM,
@ -650,6 +912,8 @@ const DictationPage: React.FC = (): JSX.Element => {
filter, filter,
direction, direction,
paramName, paramName,
authorId: payloadAuthorId,
fileName: payloadFileName,
}) })
); );
dispatch(listTypistsAsync()); dispatch(listTypistsAsync());
@ -682,125 +946,171 @@ const DictationPage: React.FC = (): JSX.Element => {
<section className={styles.dictation}> <section className={styles.dictation}>
<div> <div>
<DisPlayInfo /> <DisPlayInfo />
<ul className={styles.tableFilter}> <ul className={styles.menuAction}>
<li>{t(getTranslationID("dictationPage.label.filter"))}:</li>
<li> <li>
<label htmlFor="uploaded"> <ul className={styles.tableFilter}>
<input <li>
id="uploaded" {t(getTranslationID("dictationPage.label.filter"))}:
type="checkbox" </li>
value="flUploaded" <li>
className={styles.formCheck} <label htmlFor="uploaded">
checked={filterUploaded} <input
disabled={isLoading} id="uploaded"
onChange={(e) => { type="checkbox"
setFilterUploaded(e.target.checked); value="flUploaded"
updateFilter( className={styles.formCheck}
e.target.checked, checked={filterUploaded}
filterInProgress, disabled={isLoading}
filterPending, onChange={(e) => {
filterFinished, setFilterUploaded(e.target.checked);
filterBackup updateFilter(
); e.target.checked,
}} filterInProgress,
/> filterPending,
{t(getTranslationID("dictationPage.label.uploaded"))} filterFinished,
</label> 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>
</li> </li>
<li> <li className={styles.floatRight}>
<label htmlFor="inProgress"> <form
className={styles.searchBar}
onSubmit={(e) => requestSearch(e)}
>
<input <input
id="inProgress" type="text"
type="checkbox" placeholder={t(
value="flInProgress" getTranslationID("dictationPage.label.fileName")
className={styles.formCheck} )}
checked={filterInProgress} value={filterConditionFileName}
disabled={isLoading} onChange={(e) => onChangeFilterConditionFileName(e)}
onChange={(e) => { className={styles.searchInput}
setFilterInProgress(e.target.checked);
updateFilter(
filterUploaded,
e.target.checked,
filterPending,
filterFinished,
filterBackup
);
}}
/> />
{t(getTranslationID("dictationPage.label.inProgress"))}
</label>
</li>
<li>
<label htmlFor="pending">
<input <input
id="pending" type="text"
type="checkbox" placeholder={t(
value="flPending" getTranslationID("dictationPage.label.authorId")
className={styles.formCheck} )}
checked={filterPending} value={filterConditionAuthorId}
disabled={isLoading} onChange={(e) => onChangeFilterConditionAuthorId(e)}
onChange={(e) => { className={styles.searchInput}
setFilterPending(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
e.target.checked,
filterFinished,
filterBackup
);
}}
/> />
{t(getTranslationID("dictationPage.label.pending"))}
</label> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
</li> <button
<li> type="submit"
<label htmlFor="finished"> className={`${styles.menuLink} ${!isLoading ? styles.isActive : ""
<input }`}
id="finished" >
type="checkbox" <img
value="flFinished" src={searchIcon}
className={styles.formCheck} alt="search"
checked={filterFinished} className={styles.menuIcon}
disabled={isLoading} />
onChange={(e) => { {t(getTranslationID("dictationPage.label.search"))}
setFilterFinished(e.target.checked); </button>
updateFilter( </form>
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> </li>
</ul> </ul>
<div className={styles.tableWrap}> <div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.dictation}`}> <table className={`${styles.table} ${styles.dictation}`}>
<tr className={styles.tableHeader}> <tr className={styles.tableHeader}>
@ -1227,7 +1537,7 @@ const DictationPage: React.FC = (): JSX.Element => {
<a <a
className={ className={
x.status !== STATUS.UPLOADED || x.status !== STATUS.UPLOADED ||
!(isAdmin || isAuthor) !(isAdmin || isAuthor)
? styles.isDisable ? styles.isDisable
: "" : ""
} }
@ -1248,7 +1558,7 @@ const DictationPage: React.FC = (): JSX.Element => {
className={ className={
(x.status === STATUS.INPROGRESS || (x.status === STATUS.INPROGRESS ||
x.status === STATUS.PENDING) && x.status === STATUS.PENDING) &&
(isAdmin || isTypist) (isAdmin || isTypist)
? "" ? ""
: styles.isDisable : styles.isDisable
} }
@ -1263,14 +1573,35 @@ const DictationPage: React.FC = (): JSX.Element => {
)} )}
</a> </a>
</li> </li>
<li>
{/* タスクのステータスがFinishedかつ、ログインユーザーがAdminかTypistの場合、Change status to Pendingボタンを活性化する */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={
x.status === STATUS.FINISHED &&
(isAdmin || isTypist)
? ""
: styles.isDisable
}
onClick={() => {
onReopen(x.audioFileId);
}}
>
{t(
getTranslationID(
"dictationPage.label.reopenDictation"
)
)}
</a>
</li>
<li> <li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
// タスクのステータスがInprogressまたはPending以外の場合、削除ボタンを活性化する // タスクのステータスがInprogressまたはPending以外の場合、削除ボタンを活性化する
className={ className={
isDeletableRole && isDeletableRole &&
x.status !== STATUS.INPROGRESS && x.status !== STATUS.INPROGRESS &&
x.status !== STATUS.PENDING x.status !== STATUS.PENDING
? "" ? ""
: styles.isDisable : styles.isDisable
} }
@ -1466,18 +1797,16 @@ const DictationPage: React.FC = (): JSX.Element => {
)}`}</span> )}`}</span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
className={`${ className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
!isLoading && currentPage !== 1 ? styles.isActive : "" }`}
}`}
onClick={getFirstPage} onClick={getFirstPage}
> >
« «
</a> </a>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
className={`${ className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
!isLoading && currentPage !== 1 ? styles.isActive : "" }`}
}`}
onClick={getPrevPage} onClick={getPrevPage}
> >
@ -1485,22 +1814,20 @@ const DictationPage: React.FC = (): JSX.Element => {
{`${currentPage} of ${totalPage}`} {`${currentPage} of ${totalPage}`}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
className={`${ className={`${!isLoading && currentPage < totalPage
!isLoading && currentPage < totalPage
? styles.isActive ? styles.isActive
: "" : ""
}`} }`}
onClick={getNextPage} onClick={getNextPage}
> >
</a> </a>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
className={`${ className={`${!isLoading && currentPage < totalPage
!isLoading && currentPage < totalPage
? styles.isActive ? styles.isActive
: "" : ""
}`} }`}
onClick={getLastPage} onClick={getLastPage}
> >
» »
@ -1528,9 +1855,8 @@ const DictationPage: React.FC = (): JSX.Element => {
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a <a
onClick={onClickBackup} onClick={onClickBackup}
className={`${styles.menuLink} ${ className={`${styles.menuLink} ${isAdmin ? styles.isActive : ""
isAdmin ? styles.isActive : "" }`}
}`}
> >
<img src={download} alt="" className={styles.menuIcon} /> <img src={download} alt="" className={styles.menuIcon} />
{t(getTranslationID("dictationPage.label.fileBackup"))} {t(getTranslationID("dictationPage.label.fileBackup"))}

View File

@ -12,6 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
import { import {
LIMIT_ORDER_HISORY_NUM, LIMIT_ORDER_HISORY_NUM,
STATUS, STATUS,
LICENSE_TYPE,
getLicenseOrderHistoriesAsync, getLicenseOrderHistoriesAsync,
selectCurrentPage, selectCurrentPage,
selectIsLoading, selectIsLoading,
@ -25,20 +26,21 @@ import {
selectCompanyName, selectCompanyName,
cancelIssueAsync, cancelIssueAsync,
} from "features/license/licenseOrderHistory"; } from "features/license/licenseOrderHistory";
import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors"; import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate"; import { DelegationBar } from "components/delegate";
import { LicenseOrder, SearchPartner, PartnerLicenseInfo } from "api/api";
import undo from "../../assets/images/undo.svg"; import undo from "../../assets/images/undo.svg";
import history from "../../assets/images/history.svg"; import history from "../../assets/images/history.svg";
import progress_activit from "../../assets/images/progress_activit.svg"; import progress_activit from "../../assets/images/progress_activit.svg";
interface LicenseOrderHistoryProps { interface LicenseOrderHistoryProps {
onReturn: () => void; onReturn: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
} }
export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = ( export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
props props
): JSX.Element => { ): JSX.Element => {
const { onReturn } = props; const { onReturn, selectedRow } = props;
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation(); const [t] = useTranslation();
const total = useSelector(selectTotal); const total = useSelector(selectTotal);
@ -46,7 +48,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
const offset = useSelector(selectOffset); const offset = useSelector(selectOffset);
const currentPage = useSelector(selectCurrentPage); const currentPage = useSelector(selectCurrentPage);
const isLoading = useSelector(selectIsLoading); const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow);
// 代行操作用のトークンを取得する // 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken); const delegationAccessToken = useSelector(selectDelegationAccessToken);
@ -64,6 +65,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getLicenseOrderHistoriesAsync({ getLicenseOrderHistoriesAsync({
limit: LIMIT_ORDER_HISORY_NUM, limit: LIMIT_ORDER_HISORY_NUM,
offset, offset,
selectedRow,
}) })
); );
}; };
@ -151,11 +153,15 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getLicenseOrderHistoriesAsync({ getLicenseOrderHistoriesAsync({
limit: LIMIT_ORDER_HISORY_NUM, limit: LIMIT_ORDER_HISORY_NUM,
offset, offset,
selectedRow,
}) })
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, currentPage]); }, [dispatch, currentPage]);
const isNotTrialLicense = (license: LicenseOrder) =>
license.type !== LICENSE_TYPE.TRIAL;
return ( return (
<div <div
className={`${styles.wrap} ${delegationAccessToken ? styles.manage : ""}`} className={`${styles.wrap} ${delegationAccessToken ? styles.manage : ""}`}
@ -208,6 +214,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getTranslationID("orderHistoriesPage.label.issueDate") getTranslationID("orderHistoriesPage.label.issueDate")
)} )}
</th> </th>
<th>
{t(
getTranslationID("orderHistoriesPage.label.licenseType")
)}
</th>
<th> <th>
{t( {t(
getTranslationID( getTranslationID(
@ -229,9 +240,10 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<tr> <tr>
<td>{x.orderDate}</td> <td>{x.orderDate}</td>
<td>{x.issueDate ? x.issueDate : "-"}</td> <td>{x.issueDate ?? "-"}</td>
<td>{x.type}</td>
<td>{x.numberOfOrder}</td> <td>{x.numberOfOrder}</td>
<td>{x.poNumber}</td> <td>{x.poNumber ?? "-"}</td>
<td> <td>
{(() => { {(() => {
switch (x.status) { switch (x.status) {
@ -259,7 +271,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
})()} })()}
</td> </td>
<td> <td>
{!selectedRow && ( {!selectedRow && isNotTrialLicense(x) && (
<ul <ul
className={`${styles.menuAction} ${styles.inTable}`} className={`${styles.menuAction} ${styles.inTable}`}
> >
@ -284,7 +296,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
</li> </li>
</ul> </ul>
)} )}
{selectedRow && ( {selectedRow && isNotTrialLicense(x) && (
<ul <ul
className={`${styles.menuAction} ${styles.inTable}`} className={`${styles.menuAction} ${styles.inTable}`}
> >

View File

@ -14,11 +14,11 @@ import {
selectIsLoading, selectIsLoading,
updateRestrictionStatusAsync, updateRestrictionStatusAsync,
} from "features/license/licenseSummary"; } from "features/license/licenseSummary";
import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors"; import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate"; import { DelegationBar } from "components/delegate";
import { TIERS } from "components/auth/constants"; import { TIERS } from "components/auth/constants";
import { isAdminUser, isApproveTier } from "features/auth/utils"; import { isAdminUser, isApproveTier } from "features/auth/utils";
import { PartnerLicenseInfo, SearchPartner } from "../../api";
import postAdd from "../../assets/images/post_add.svg"; import postAdd from "../../assets/images/post_add.svg";
import history from "../../assets/images/history.svg"; import history from "../../assets/images/history.svg";
import key from "../../assets/images/key.svg"; import key from "../../assets/images/key.svg";
@ -27,19 +27,20 @@ import circle from "../../assets/images/circle.svg";
import returnLabel from "../../assets/images/undo.svg"; import returnLabel from "../../assets/images/undo.svg";
import { LicenseOrderPopup } from "./licenseOrderPopup"; import { LicenseOrderPopup } from "./licenseOrderPopup";
import { CardLicenseActivatePopup } from "./cardLicenseActivatePopup"; import { CardLicenseActivatePopup } from "./cardLicenseActivatePopup";
import { TrialLicenseIssuePopup } from "./trialLicenseIssuePopup";
// eslint-disable-next-line import/no-named-as-default // eslint-disable-next-line import/no-named-as-default
import LicenseOrderHistory from "./licenseOrderHistory"; import LicenseOrderHistory from "./licenseOrderHistory";
interface LicenseSummaryProps { interface LicenseSummaryProps {
onReturn?: () => void; onReturn?: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
} }
export const LicenseSummary: React.FC<LicenseSummaryProps> = ( export const LicenseSummary: React.FC<LicenseSummaryProps> = (
props props
): JSX.Element => { ): JSX.Element => {
const { onReturn } = props; const { onReturn, selectedRow } = props;
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation(); const [t] = useTranslation();
const selectedRow = useSelector(selectSelectedRow);
// 代行操作用のトークンを取得する // 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken); const delegationAccessToken = useSelector(selectDelegationAccessToken);
@ -49,6 +50,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false); const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] = const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
useState(false); useState(false);
const [isTrialLicenseIssuePopupOpen, setIsTrialLicenseIssuePopupOpen] =
useState(false);
const onlicenseOrderOpen = useCallback(() => { const onlicenseOrderOpen = useCallback(() => {
setIslicenseOrderPopupOpen(true); setIslicenseOrderPopupOpen(true);
@ -58,6 +61,10 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
setIsCardLicenseActivatePopupOpen(true); setIsCardLicenseActivatePopupOpen(true);
}, [setIsCardLicenseActivatePopupOpen]); }, [setIsCardLicenseActivatePopupOpen]);
const onTrialLicenseIssueOpen = useCallback(() => {
setIsTrialLicenseIssuePopupOpen(true);
}, [setIsTrialLicenseIssuePopupOpen]);
// 呼び出し画面制御関係 // 呼び出し画面制御関係
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] = const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
useState(false); useState(false);
@ -71,6 +78,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
const companyName = useSelector(selectCompanyName); const companyName = useSelector(selectCompanyName);
const isTier1 = isApproveTier([TIERS.TIER1]); const isTier1 = isApproveTier([TIERS.TIER1]);
const isTier2 = isApproveTier([TIERS.TIER2]);
const isAdmin = isAdminUser(); const isAdmin = isAdminUser();
useEffect(() => { useEffect(() => {
@ -133,11 +141,21 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}} }}
/> />
)} )}
{isTrialLicenseIssuePopupOpen && (
<TrialLicenseIssuePopup
onClose={() => {
setIsTrialLicenseIssuePopupOpen(false);
dispatch(getLicenseSummaryAsync({ selectedRow }));
}}
selectedRow={selectedRow}
/>
)}
{islicenseOrderHistoryOpen && ( {islicenseOrderHistoryOpen && (
<LicenseOrderHistory <LicenseOrderHistory
onReturn={() => { onReturn={() => {
setIsLicenseOrderHistoryOpen(false); setIsLicenseOrderHistoryOpen(false);
}} }}
selectedRow={selectedRow}
/> />
)} )}
{!islicenseOrderHistoryOpen && ( {!islicenseOrderHistoryOpen && (
@ -228,6 +246,30 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
</a> </a>
)} )}
</li> </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> </ul>
<div className={styles.marginRgt3}> <div className={styles.marginRgt3}>
<dl <dl

View File

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

View File

@ -0,0 +1,354 @@
import { SearchPartner } from "api";
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from "react";
import { AppDispatch } from "app/store";
import styles from "styles/app.module.scss";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { useSelector, useDispatch } from "react-redux";
import {
changeSelectedRow,
cleanupSearchResult,
cleanupPartnerHierarchy,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
searchPartnersAsync,
selectIsLoading,
selectSelectedRow,
selectSearchResult,
selectPartnerHierarchy,
selectIsLicenseOrderHistoryOpen,
selectIsViewDetailsOpen,
getPartnerHierarchy,
} from "features/license/searchPartner";
import { setIsSearchPopupOpen } from "features/license/partnerLicense";
import { LicenseSummary } from "./licenseSummary";
import { LicenseOrderHistory } from "./licenseOrderHistory";
import close from "../../assets/images/close.svg";
import searchIcon from "../../assets/images/search.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface SearchPopupProps {
onClose: () => void;
}
export const SearchPartnerPopup: React.FC<SearchPopupProps> = (props) => {
const dispatch: AppDispatch = useDispatch();
const { onClose } = props;
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow) as SearchPartner;
const searchResult = useSelector(selectSearchResult);
const partnerHierarchy = useSelector(selectPartnerHierarchy);
const [accountId, setAccountId] = useState("");
const [companyName, setCompanyName] = useState("");
const [isBreadcrumbOpen, setIsBreadcrumbOpen] = useState(false);
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
const isLicenseOrderHistoryOpen = useSelector(
selectIsLicenseOrderHistoryOpen
);
const [popupPosition, setPopupPosition] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const breadcrumbRef = useRef<HTMLDivElement>(null);
// フォームの入力チェック
const searchButtonEnabled = useMemo(() => {
// 両方入力がない場合はボタンを活性化しない。
if (!companyName && !accountId) {
return false;
}
// Company Nameは3文字以上入力がない場合はボタンを活性化しない。
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
if (companyName && [...companyName].length <= 2) {
return false;
}
// Account IDは数字ではないまたは0以下の場合はボタンを活性化しない。
if (
accountId &&
(Number.isNaN(Number(accountId)) || Number(accountId) <= 0)
) {
return false;
}
return true;
}, [companyName, accountId]);
const requestSearch = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!searchButtonEnabled) return;
dispatch(
searchPartnersAsync({
companyName,
accountId: Number(accountId),
})
);
},
[dispatch, companyName, accountId, searchButtonEnabled]
);
const handleAccountNameClick = useCallback(
async (clickAccountId: number, event: React.MouseEvent) => {
// ロード中はなにもしない。
if (isLoading) return;
event.stopPropagation();
// アカウントの階層を取得
await dispatch(getPartnerHierarchy({ accountId: clickAccountId }));
setPopupPosition({ x: event.clientX, y: event.clientY });
setIsBreadcrumbOpen(true);
},
[dispatch, setPopupPosition, setIsBreadcrumbOpen, isLoading]
);
const closeBreadcrumbPopup = () => {
setIsBreadcrumbOpen(false);
};
// ポップアップ外のクリックポップアップ非表示するイベント
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
breadcrumbRef.current &&
!breadcrumbRef.current.contains(event.target as Node)
) {
closeBreadcrumbPopup();
}
};
if (isBreadcrumbOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isBreadcrumbOpen]);
const tierNames: { [key: number]: string } = {
// eslint-disable-next-line @typescript-eslint/naming-convention
1: t(getTranslationID("common.label.tier1")),
// eslint-disable-next-line @typescript-eslint/naming-convention
2: t(getTranslationID("common.label.tier2")),
// eslint-disable-next-line @typescript-eslint/naming-convention
3: t(getTranslationID("common.label.tier3")),
// eslint-disable-next-line @typescript-eslint/naming-convention
4: t(getTranslationID("common.label.tier4")),
// eslint-disable-next-line @typescript-eslint/naming-convention
5: t(getTranslationID("common.label.tier5")),
};
const openViewDetails = useCallback(
(value: SearchPartner) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsViewDetailsOpen({ value: true }));
},
[dispatch]
);
const openOrderHistory = useCallback(
(value?: SearchPartner) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
},
[dispatch]
);
const handleModalClose = useCallback(() => {
// ポップアップ閉じたら、検索結果をクリアする
dispatch(cleanupSearchResult());
dispatch(cleanupPartnerHierarchy());
onClose();
dispatch(setIsSearchPopupOpen({ value: false }));
}, [dispatch, onClose]);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div>
{isViewDetailsOpen && (
<div>
<LicenseSummary
onReturn={() => dispatch(setIsViewDetailsOpen({ value: false }))}
selectedRow={selectedRow}
/>
</div>
)}
{isLicenseOrderHistoryOpen && (
<div>
<LicenseOrderHistory
onReturn={() =>
dispatch(setIsLicenseOrderHistoryOpen({ value: false }))
}
selectedRow={selectedRow}
/>
</div>
)}
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.searchModalBox}>
<div className={styles.headerContainer}>
<p className={styles.modalTitle}>
{t(getTranslationID("searchPartnerAccountPopupPage.label.title"))}
</p>
<form
className={styles.searchBar}
onSubmit={(e) => requestSearch(e)}
>
<input
type="text"
placeholder={t(getTranslationID("partnerLicense.label.name"))}
value={companyName}
onChange={(e) => setCompanyName(e.target.value.trimStart())}
className={styles.searchInput}
/>
<input
type="text"
placeholder={t(
getTranslationID("partnerLicense.label.accountId")
)}
value={accountId}
onChange={(e) => setAccountId(e.target.value.trimStart())}
className={styles.searchInput}
/>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<button
className={`${styles.menuLink} ${!isLoading && searchButtonEnabled ? styles.isActive : ""
}`}
type="submit"
disabled={!searchButtonEnabled || isLoading}
>
<img
src={searchIcon}
alt="search"
className={styles.menuIcon}
/>
{t(getTranslationID("partnerLicense.label.search"))}
</button>
<button type="button" onClick={handleModalClose}>
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
/>
</button>
</form>
</div>
<table
className={`${styles.table} ${styles.partner} ${styles.marginBtm3}`}
>
<tr className={styles.tableHeader}>
<th>
<a>{t(getTranslationID("partnerPage.label.name"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.category"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.accountId"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.country"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.primaryAdmin"))}</a>
</th>
<th>
<a>{t(getTranslationID("partnerPage.label.email"))}</a>
</th>
<th>
<a>{"" /** Order History、View Details用の空カラム */}</a>
</th>
</tr>
{searchResult.map((result) => (
<tr key={result.accountId}>
<td
onClick={(event) =>
handleAccountNameClick(result.accountId, event)
}
className={styles.hoverBlue}
>
{result.name}
</td>
<td>{tierNames[result.tier]}</td>
<td>{result.accountId}</td>
<td>{result.country}</td>
<td>{result.primaryAdmin}</td>
<td>{result.email ?? "-"}</td>
<td>
<ul className={`${styles.menuAction} ${styles.inTable}`}>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
openOrderHistory(result);
}}
>
{t(
getTranslationID(
"partnerLicense.label.orderHistoryButton"
)
)}
</a>
</li>
{result.tier === 5 && (
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
openViewDetails(result);
}}
>
{t(
getTranslationID("partnerLicense.label.viewDetails")
)}
</a>
</li>
)}
</ul>
</td>
</tr>
))}
</table>
{searchResult.length === 0 && (
<p style={{ margin: "10px", textAlign: "center" }}>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{/* ローディングオーバーレイ */}
<div
style={{ display: isLoading ? "inline" : "none" }}
className={styles.overlay}
>
<img
src={progress_activit}
className={`${styles.icLoading} ${styles.alignCenter}`}
alt="Loading"
/>
</div>
</div>
{isBreadcrumbOpen && (
<div
ref={breadcrumbRef}
className={styles.breadcrumbPopup}
style={{ top: popupPosition.y, left: popupPosition.x }}
>
<ul className={styles.brCrumbPartner}>
{partnerHierarchy.map((parent) => (
<li key={parent.tier}>
<span>{parent.name}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,123 @@
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import {
issueTrialLicenseAsync,
cleanupApps,
selectIsLoading,
selectNumberOfLicenses,
selectExpirationDate,
setExpirationDate,
} from "features/license/licenseTrialIssue";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import { SearchPartner, PartnerLicenseInfo } from "../../api";
interface TrialLicenseIssuePopupProps {
onClose: () => void;
selectedRow?: PartnerLicenseInfo | SearchPartner;
}
export const TrialLicenseIssuePopup: React.FC<TrialLicenseIssuePopupProps> = (
props
) => {
const { onClose, selectedRow } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
const numberOfLicenses = useSelector(selectNumberOfLicenses);
const expirationDate = useSelector(selectExpirationDate);
useEffect(
() => () => {
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
dispatch(cleanupApps());
},
[dispatch]
);
// ポップアップ表示時
useEffect(() => {
// トライアルライセンスの有効期限を設定。
dispatch(setExpirationDate());
}, [dispatch]);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
onClose();
}, [isLoading, onClose]);
// 発行ボタン押下時
const onIssueTrialLicense = useCallback(async () => {
// トライアルライセンス発行APIの呼び出し
const { meta } = await dispatch(issueTrialLicenseAsync({ selectedRow }));
if (meta.requestStatus === "fulfilled") {
closePopup();
}
}, [dispatch, closePopup, selectedRow]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("trialLicenseIssuePopupPage.label.title"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle}>
{t(getTranslationID("trialLicenseIssuePopupPage.label.subTitle"))}
</dt>
<dt className={styles.overLine}>
{t(
getTranslationID(
"trialLicenseIssuePopupPage.label.numberOfLicenses"
)
)}
</dt>
<dd>{numberOfLicenses}</dd>
<dt>
{t(
getTranslationID(
"trialLicenseIssuePopupPage.label.expirationDate"
)
)}
</dt>
<dd>{expirationDate}</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(
getTranslationID(
"trialLicenseIssuePopupPage.label.issueButton"
)
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
onClick={onIssueTrialLicense}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -7,6 +7,14 @@ import Footer from "components/footer";
const SupportPage: React.FC = () => { const SupportPage: React.FC = () => {
const { t } = useTranslation(); 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 customUlStyles: React.CSSProperties = { marginBottom: "1rem" };
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
@ -23,8 +31,8 @@ const SupportPage: React.FC = () => {
<div> <div>
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2> <h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
<div className={styles.txContents}> <div className={styles.txContents} style={userGuideDivStyles}>
<ul className={styles.listDocument}> <ul className={styles.listDocument} style={customUlStyles}>
<li> <li>
<a <a
href="https://download.omsystem.com/pages/odms_download/manual/odms_cloud/" href="https://download.omsystem.com/pages/odms_download/manual/odms_cloud/"
@ -40,6 +48,105 @@ const SupportPage: React.FC = () => {
{t(getTranslationID("supportPage.text.notResolved"))} {t(getTranslationID("supportPage.text.notResolved"))}
</p> </p>
</div> </div>
<h2>
{t(getTranslationID("supportPage.label.programDownload"))}
</h2>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_desktop/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.omdsDesktopAppDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.omdsDesktopAppDownloadLinkDescription"
)
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/device_customization_program/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(getTranslationID("supportPage.label.dcpDownloadLink"))}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.dcpDownloadLinkDescription"
)
)}
</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> </div>
</section> </section>
</div> </div>

View File

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

View File

@ -10,6 +10,7 @@ import {
selectIsLoading, selectIsLoading,
deallocateLicenseAsync, deallocateLicenseAsync,
deleteUserAsync, deleteUserAsync,
confirmUserForceAsync,
} from "features/user"; } from "features/user";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
@ -32,6 +33,7 @@ import checkFill from "../../assets/images/check_fill.svg";
import checkOutline from "../../assets/images/check_outline.svg"; import checkOutline from "../../assets/images/check_outline.svg";
import progress_activit from "../../assets/images/progress_activit.svg"; import progress_activit from "../../assets/images/progress_activit.svg";
import upload from "../../assets/images/upload.svg"; import upload from "../../assets/images/upload.svg";
import searchIcon from "../../assets/images/search.svg";
import { UserAddPopup } from "./popup"; import { UserAddPopup } from "./popup";
import { UserUpdatePopup } from "./updatePopup"; import { UserUpdatePopup } from "./updatePopup";
import { AllocateLicensePopup } from "./allocateLicensePopup"; import { AllocateLicensePopup } from "./allocateLicensePopup";
@ -48,6 +50,8 @@ const UserListPage: React.FC = (): JSX.Element => {
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] = const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
useState(false); useState(false);
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false); const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
const [searchEmail, setSearchEmail] = useState("");
const [searchUserName, setSearchUserName] = useState("");
const onOpen = useCallback(() => { const onOpen = useCallback(() => {
setIsPopupOpen(true); setIsPopupOpen(true);
@ -84,6 +88,7 @@ const UserListPage: React.FC = (): JSX.Element => {
const { meta } = await dispatch(deallocateLicenseAsync({ userId })); const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync()); dispatch(listUsersAsync());
} }
}, },
@ -102,12 +107,53 @@ const UserListPage: React.FC = (): JSX.Element => {
const { meta } = await dispatch(deleteUserAsync({ userId })); const { meta } = await dispatch(deleteUserAsync({ userId }));
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync()); dispatch(listUsersAsync());
} }
}, },
[dispatch, t] [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(() => { useEffect(() => {
// ユーザ一覧取得処理を呼び出す // ユーザ一覧取得処理を呼び出す
dispatch(listUsersAsync()); dispatch(listUsersAsync());
@ -126,18 +172,21 @@ const UserListPage: React.FC = (): JSX.Element => {
onClose={() => { onClose={() => {
setIsUpdatePopupOpen(false); setIsUpdatePopupOpen(false);
}} }}
clearUserSearchInputs={clearUserSearchInputs}
/> />
<UserAddPopup <UserAddPopup
isOpen={isPopupOpen} isOpen={isPopupOpen}
onClose={() => { onClose={() => {
setIsPopupOpen(false); setIsPopupOpen(false);
}} }}
clearUserSearchInputs={clearUserSearchInputs}
/> />
<AllocateLicensePopup <AllocateLicensePopup
isOpen={isAllocateLicensePopupOpen} isOpen={isAllocateLicensePopupOpen}
onClose={() => { onClose={() => {
setIsAllocateLicensePopupOpen(false); setIsAllocateLicensePopupOpen(false);
}} }}
clearUserSearchInputs={clearUserSearchInputs}
/> />
<ImportPopup <ImportPopup
isOpen={isImportPopupOpen} isOpen={isImportPopupOpen}
@ -185,6 +234,50 @@ const UserListPage: React.FC = (): JSX.Element => {
{t(getTranslationID("userListPage.label.bulkImport"))} {t(getTranslationID("userListPage.label.bulkImport"))}
</a> </a>
</li> </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> </ul>
<div className={styles.tableWrap}> <div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.user}`}> <table className={`${styles.table} ${styles.user}`}>
@ -308,6 +401,23 @@ const UserListPage: React.FC = (): JSX.Element => {
)} )}
</a> </a>
</li> </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> </ul>
</td> </td>
<td> {user.name}</td> <td> {user.name}</td>

View File

@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
interface UserAddPopupProps { interface UserAddPopupProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
clearUserSearchInputs: () => void;
} }
export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => { export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
const { isOpen, onClose } = props; const { isOpen, onClose, clearUserSearchInputs } = props;
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true); const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
@ -75,6 +76,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
closePopup(); closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync()); dispatch(listUsersAsync());
} }
}, [ }, [

View File

@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
interface UserUpdatePopupProps { interface UserUpdatePopupProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
clearUserSearchInputs: () => void;
} }
export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => { export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
const { isOpen, onClose } = props; const { isOpen, onClose, clearUserSearchInputs } = props;
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const closePopup = useCallback(() => { const closePopup = useCallback(() => {
@ -79,6 +80,7 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
closePopup(); closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync()); dispatch(listUsersAsync());
} }
}, [ }, [

View File

@ -454,7 +454,6 @@ h3 + .brCrumb .tlIcon {
.brCrumbLicense li a:hover { .brCrumbLicense li a:hover {
color: #0084b2; color: #0084b2;
} }
.buttonNormal { .buttonNormal {
display: inline-block; display: inline-block;
width: 15rem; width: 15rem;
@ -1699,9 +1698,9 @@ _:-ms-lang(x)::-ms-backdrop,
margin-left: 648px; margin-left: 648px;
text-align: right; text-align: right;
} }
.menuAction { .menuAction {
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
position: relative;
} }
.menuAction li { .menuAction li {
display: inline-block; display: inline-block;
@ -2059,6 +2058,9 @@ tr.isSelected .menuInTable li a.isDisable {
height: 34px; height: 34px;
position: relative; position: relative;
} }
.dictation .menuAction:not(:first-child) {
margin-top: 0.6rem;
}
.dictation .menuAction .alignLeft { .dictation .menuAction .alignLeft {
position: absolute; position: absolute;
left: 0; left: 0;
@ -2765,4 +2767,106 @@ tr.isSelected .menuInTable li a.isDisable {
white-space: pre-wrap; 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 */ /*# sourceMappingURL=style.css.map */

View File

@ -233,5 +233,13 @@ declare const classNames: {
readonly txContents: "txContents"; readonly txContents: "txContents";
readonly txIcon: "txIcon"; readonly txIcon: "txIcon";
readonly txWswrap: "txWswrap"; 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; export = classNames;

View File

@ -54,7 +54,7 @@
}, },
"text": { "text": {
"maintenanceNotificationTitle": "Hinweis auf geplante Wartungsarbeiten", "maintenanceNotificationTitle": "Hinweis auf geplante Wartungsarbeiten",
"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." "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."
} }
}, },
"signupPage": { "signupPage": {
@ -146,7 +146,9 @@
"duplicateEmailError": "Die E-Mail-Adressen in den folgenden Zeilen werden in der CSV-Datei dupliziert.", "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.", "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.", "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." "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."
}, },
"label": { "label": {
"title": "Benutzer", "title": "Benutzer",
@ -193,7 +195,9 @@
"encryptionLabel": "Verschlüsselung", "encryptionLabel": "Verschlüsselung",
"encryptionPasswordLabel": "Verschlüsselungspasswort", "encryptionPasswordLabel": "Verschlüsselungspasswort",
"promptLabel": "Eingabeaufforderung", "promptLabel": "Eingabeaufforderung",
"addUsers": "Benutzer hinzufügen" "addUsers": "Benutzer hinzufügen",
"forceEmailVerification": "E-Mail-Verifizierung erzwingen",
"search": "Suche"
}, },
"text": { "text": {
"downloadExplain": "Bitte laden Sie die CSV-Beispieldatei herunter und geben Sie die erforderlichen Informationen gemäß den folgenden Regeln ein.", "downloadExplain": "Bitte laden Sie die CSV-Beispieldatei herunter und geben Sie die erforderlichen Informationen gemäß den folgenden Regeln ein.",
@ -226,7 +230,8 @@
"storageAvailable": "Speicher nicht verfügbar (Menge überschritten)", "storageAvailable": "Speicher nicht verfügbar (Menge überschritten)",
"licenseLabel": "Lizenz", "licenseLabel": "Lizenz",
"storageLabel": "Lagerung", "storageLabel": "Lagerung",
"storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung" "storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung",
"issueTrialLicense": "Testlizenz ausstellen"
}, },
"message": { "message": {
"storageUnavalableSwitchingConfirm": "Sind Sie sicher, dass Sie den Speichernutzungsstatus für dieses Konto ändern möchten?" "storageUnavalableSwitchingConfirm": "Sind Sie sicher, dass Sie den Speichernutzungsstatus für dieses Konto ändern möchten?"
@ -257,6 +262,7 @@
"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.", "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.", "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.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
"reopenFailedError": "Der Status kann nicht in „Ausstehend“ geändert werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
"deleteFailedError": "Die Aufgabe konnte nicht gelöscht werden. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "deleteFailedError": "Die Aufgabe konnte nicht gelöscht werden. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.",
"licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.",
"licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.", "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.",
@ -310,7 +316,9 @@
"applications": "Desktopanwendung", "applications": "Desktopanwendung",
"cancelDictation": "Transkription abbrechen", "cancelDictation": "Transkription abbrechen",
"rawFileName": "Ursprünglicher Dateiname", "rawFileName": "Ursprünglicher Dateiname",
"fileNameSave": "Führen Sie eine Dateiumbenennung durch" "fileNameSave": "Führen Sie eine Dateiumbenennung durch",
"reopenDictation": "Status auf „Ausstehend“ ändern",
"search": "Suche"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -380,7 +388,9 @@
"issueRequesting": "Lizenzen auf Bestellung", "issueRequesting": "Lizenzen auf Bestellung",
"viewDetails": "Details anzeigen", "viewDetails": "Details anzeigen",
"accounts": "konten", "accounts": "konten",
"changeOwnerButton": "Change Owner" "changeOwnerButton": "Change Owner",
"allocatedLicense": "Zugewiesene Lizenzen",
"search": "Suche"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -398,7 +408,8 @@
"issue": "Ausgabe", "issue": "Ausgabe",
"issueCancel": "Lizenzen kündigen", "issueCancel": "Lizenzen kündigen",
"orderCancel": "Bestellung stornieren", "orderCancel": "Bestellung stornieren",
"histories": "geschichten" "histories": "geschichten",
"licenseType": "Lizenztyp"
}, },
"message": { "message": {
"notEnoughOfNumberOfLicense": "Lizenzen konnten aufgrund unzureichender Lizenzanzahl nicht ausgestellt werden. Bitte bestellen Sie zusätzliche Lizenzen.", "notEnoughOfNumberOfLicense": "Lizenzen konnten aufgrund unzureichender Lizenzanzahl nicht ausgestellt werden. Bitte bestellen Sie zusätzliche Lizenzen.",
@ -617,7 +628,16 @@
"label": { "label": {
"title": "Support", "title": "Support",
"howToUse": "So verwenden Sie das System", "howToUse": "So verwenden Sie das System",
"supportPageLink": "OMDS Cloud-Benutzerhandbuch" "supportPageLink": "OMDS Cloud-Benutzerhandbuch",
"programDownload": "Programm-Download",
"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."
}, },
"text": { "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." "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."
@ -653,5 +673,22 @@
"upperLayerId": "Upper Layer ID", "upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower 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": { "text": {
"maintenanceNotificationTitle": "Notice of scheduled maintenance", "maintenanceNotificationTitle": "Notice of scheduled 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." "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."
} }
}, },
"signupPage": { "signupPage": {
@ -146,7 +146,9 @@
"duplicateEmailError": "The email addresses in the following lines are duplicated in the CSV file.", "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.", "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.", "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." "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."
}, },
"label": { "label": {
"title": "User", "title": "User",
@ -193,7 +195,9 @@
"encryptionLabel": "Encryption", "encryptionLabel": "Encryption",
"encryptionPasswordLabel": "Encryption Password", "encryptionPasswordLabel": "Encryption Password",
"promptLabel": "Prompt", "promptLabel": "Prompt",
"addUsers": "Add User" "addUsers": "Add User",
"forceEmailVerification": "Force Email Verification",
"search": "Search"
}, },
"text": { "text": {
"downloadExplain": "Please download the sample CSV file and apply the required information according to the rules below.", "downloadExplain": "Please download the sample CSV file and apply the required information according to the rules below.",
@ -226,7 +230,8 @@
"storageAvailable": "Storage Unavailable (Exceeded Amount)", "storageAvailable": "Storage Unavailable (Exceeded Amount)",
"licenseLabel": "License", "licenseLabel": "License",
"storageLabel": "Storage", "storageLabel": "Storage",
"storageUnavailableCheckbox": "Restrict account usage" "storageUnavailableCheckbox": "Restrict account usage",
"issueTrialLicense": "Issue Trial License"
}, },
"message": { "message": {
"storageUnavalableSwitchingConfirm": "Are you sure you would like to change the storage usage status for this account?" "storageUnavalableSwitchingConfirm": "Are you sure you would like to change the storage usage status for this account?"
@ -257,6 +262,7 @@
"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.", "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.", "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.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
"reopenFailedError": "The status could not be changed to Pending. Please refresh the screen to see the current status. Only files with Finished status can be operated.",
"deleteFailedError": "Failed to delete the task. Please refresh the screen and check again.", "deleteFailedError": "Failed to delete the task. Please refresh the screen and check again.",
"licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.", "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.",
"licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.", "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.",
@ -310,7 +316,9 @@
"applications": "Desktop Application", "applications": "Desktop Application",
"cancelDictation": "Cancel Transcription", "cancelDictation": "Cancel Transcription",
"rawFileName": "Original File Name", "rawFileName": "Original File Name",
"fileNameSave": "Execute file rename" "fileNameSave": "Execute file rename",
"reopenDictation": "Change status to Pending",
"search": "Search"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -380,7 +388,9 @@
"issueRequesting": "Licenses on Order", "issueRequesting": "Licenses on Order",
"viewDetails": "View Details", "viewDetails": "View Details",
"accounts": "accounts", "accounts": "accounts",
"changeOwnerButton": "Change Owner" "changeOwnerButton": "Change Owner",
"allocatedLicense": "Allocated Licenses",
"search": "Search"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -398,7 +408,8 @@
"issue": "Issue", "issue": "Issue",
"issueCancel": "Cancel Licenses", "issueCancel": "Cancel Licenses",
"orderCancel": "Cancel Order", "orderCancel": "Cancel Order",
"histories": "histories" "histories": "histories",
"licenseType": "License Type"
}, },
"message": { "message": {
"notEnoughOfNumberOfLicense": "Licenses could not be issued due to insufficient amount of licenses. Please order additional licenses.", "notEnoughOfNumberOfLicense": "Licenses could not be issued due to insufficient amount of licenses. Please order additional licenses.",
@ -617,7 +628,16 @@
"label": { "label": {
"title": "Support", "title": "Support",
"howToUse": "How to use the system", "howToUse": "How to use the system",
"supportPageLink": "OMDS Cloud User Guide" "supportPageLink": "OMDS Cloud User Guide",
"programDownload": "Program Download",
"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."
}, },
"text": { "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." "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."
@ -653,5 +673,22 @@
"upperLayerId": "Upper Layer ID", "upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower 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": { "text": {
"maintenanceNotificationTitle": "Aviso de mantenimiento programado", "maintenanceNotificationTitle": "Aviso de mantenimiento programado",
"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." "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."
} }
}, },
"signupPage": { "signupPage": {
@ -146,7 +146,9 @@
"duplicateEmailError": "Las direcciones de correo electrónico de las siguientes líneas están duplicadas en el archivo CSV.", "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.", "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.", "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." "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."
}, },
"label": { "label": {
"title": "Usuario", "title": "Usuario",
@ -193,7 +195,9 @@
"encryptionLabel": "Cifrado ", "encryptionLabel": "Cifrado ",
"encryptionPasswordLabel": "Contraseña de cifrado", "encryptionPasswordLabel": "Contraseña de cifrado",
"promptLabel": "Solicitar", "promptLabel": "Solicitar",
"addUsers": "Agregar usuario" "addUsers": "Agregar usuario",
"forceEmailVerification": "Verificación forzada de correo electrónico",
"search": "Búsqueda"
}, },
"text": { "text": {
"downloadExplain": "Descargue el archivo CSV de muestra y aplique la información requerida de acuerdo con las reglas siguientes.", "downloadExplain": "Descargue el archivo CSV de muestra y aplique la información requerida de acuerdo con las reglas siguientes.",
@ -226,7 +230,8 @@
"storageAvailable": "Almacenamiento no disponible (cantidad excedida)", "storageAvailable": "Almacenamiento no disponible (cantidad excedida)",
"licenseLabel": "Licencia", "licenseLabel": "Licencia",
"storageLabel": "Almacenamiento", "storageLabel": "Almacenamiento",
"storageUnavailableCheckbox": "Restringir el uso de la cuenta" "storageUnavailableCheckbox": "Restringir el uso de la cuenta",
"issueTrialLicense": "Emitir licencia de prueba"
}, },
"message": { "message": {
"storageUnavalableSwitchingConfirm": "¿Está seguro de que desea cambiar el estado de uso del almacenamiento de esta cuenta?" "storageUnavalableSwitchingConfirm": "¿Está seguro de que desea cambiar el estado de uso del almacenamiento de esta cuenta?"
@ -257,6 +262,7 @@
"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.", "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.", "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.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
"reopenFailedError": "No se pudo cambiar el estado a Pendiente. Actualice la pantalla para ver el estado actual. Solo se pueden utilizar los archivos con estado Finalizado.",
"deleteFailedError": "No se pudo eliminar la tarea. Actualice la pantalla y verifique nuevamente.", "deleteFailedError": "No se pudo eliminar la tarea. Actualice la pantalla y verifique nuevamente.",
"licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.", "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.",
"licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.", "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.",
@ -310,7 +316,9 @@
"applications": "Aplicación de escritorio", "applications": "Aplicación de escritorio",
"cancelDictation": "Cancelar transcripción", "cancelDictation": "Cancelar transcripción",
"rawFileName": "Nombre de archivo original", "rawFileName": "Nombre de archivo original",
"fileNameSave": "Ejecutar cambio de nombre de archivo" "fileNameSave": "Ejecutar cambio de nombre de archivo",
"reopenDictation": "Cambiar el estado a Pendiente",
"search": "Búsqueda"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -380,7 +388,9 @@
"issueRequesting": "Licencias en Pedido", "issueRequesting": "Licencias en Pedido",
"viewDetails": "Ver detalles", "viewDetails": "Ver detalles",
"accounts": "cuentas", "accounts": "cuentas",
"changeOwnerButton": "Change Owner" "changeOwnerButton": "Change Owner",
"allocatedLicense": "Licencias asignadas",
"search": "Búsqueda"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -398,7 +408,8 @@
"issue": "Emitida", "issue": "Emitida",
"issueCancel": "Cancelar licencias", "issueCancel": "Cancelar licencias",
"orderCancel": "Cancelar pedido", "orderCancel": "Cancelar pedido",
"histories": "historias" "histories": "historias",
"licenseType": "Tipo de licencia"
}, },
"message": { "message": {
"notEnoughOfNumberOfLicense": "No se pudieron emitir licencias debido a una cantidad insuficiente de licencias. Solicite licencias adicionales.", "notEnoughOfNumberOfLicense": "No se pudieron emitir licencias debido a una cantidad insuficiente de licencias. Solicite licencias adicionales.",
@ -617,7 +628,16 @@
"label": { "label": {
"title": "Soporte", "title": "Soporte",
"howToUse": "Cómo utilizar el sistema", "howToUse": "Cómo utilizar el sistema",
"supportPageLink": "Guía del usuario de la nube OMDS" "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.",
"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."
}, },
"text": { "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." "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."
@ -653,5 +673,22 @@
"upperLayerId": "Upper Layer ID", "upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower 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": { "text": {
"maintenanceNotificationTitle": "Avis de maintenance programmée", "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 17 juin à 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 27 janvier à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance."
} }
}, },
"signupPage": { "signupPage": {
@ -146,7 +146,9 @@
"duplicateEmailError": "Les adresses email des lignes suivantes sont dupliquées dans le fichier CSV.", "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.", "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.", "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." "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é."
}, },
"label": { "label": {
"title": "Utilisateur", "title": "Utilisateur",
@ -193,7 +195,9 @@
"encryptionLabel": "Chiffrement", "encryptionLabel": "Chiffrement",
"encryptionPasswordLabel": "Mot de passe de chiffrement", "encryptionPasswordLabel": "Mot de passe de chiffrement",
"promptLabel": "Invite", "promptLabel": "Invite",
"addUsers": "Ajouter un utilisateur" "addUsers": "Ajouter un utilisateur",
"forceEmailVerification": "Forcer la vérification de l'e-mail",
"search": "Recherche"
}, },
"text": { "text": {
"downloadExplain": "Veuillez télécharger l'exemple de fichier CSV et appliquer les informations requises conformément aux règles ci-dessous.", "downloadExplain": "Veuillez télécharger l'exemple de fichier CSV et appliquer les informations requises conformément aux règles ci-dessous.",
@ -226,7 +230,8 @@
"storageAvailable": "Stockage indisponible (montant dépassée)", "storageAvailable": "Stockage indisponible (montant dépassée)",
"licenseLabel": "Licence", "licenseLabel": "Licence",
"storageLabel": "Stockage", "storageLabel": "Stockage",
"storageUnavailableCheckbox": "Restreindre l'utilisation du compte" "storageUnavailableCheckbox": "Restreindre l'utilisation du compte",
"issueTrialLicense": "Délivrer licence d'essai"
}, },
"message": { "message": {
"storageUnavalableSwitchingConfirm": "Êtes-vous sûr de vouloir modifier l'état d'utilisation du stockage pour ce compte ?" "storageUnavalableSwitchingConfirm": "Êtes-vous sûr de vouloir modifier l'état d'utilisation du stockage pour ce compte ?"
@ -257,6 +262,7 @@
"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.", "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.", "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.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
"reopenFailedError": "Le statut n'a pas pu être modifié en Suspendu. Veuillez actualiser l'écran pour voir le statut actuel. Seuls les fichiers dont le statut est Terminé peuvent être traités.",
"deleteFailedError": "Échec de la suppression de la tâche. Veuillez actualiser l'écran et vérifier à nouveau.", "deleteFailedError": "Échec de la suppression de la tâche. Veuillez actualiser l'écran et vérifier à nouveau.",
"licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.", "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.",
"licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.", "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.",
@ -310,7 +316,9 @@
"applications": "Application de bureau", "applications": "Application de bureau",
"cancelDictation": "Annuler la transcription", "cancelDictation": "Annuler la transcription",
"rawFileName": "Nom du fichier d'origine", "rawFileName": "Nom du fichier d'origine",
"fileNameSave": "Exécuter le changement de nom du fichier" "fileNameSave": "Exécuter le changement de nom du fichier",
"reopenDictation": "Changer le statut en Suspendu",
"search": "Recherche"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -380,7 +388,9 @@
"issueRequesting": "Licences en commande", "issueRequesting": "Licences en commande",
"viewDetails": "Voir les détails", "viewDetails": "Voir les détails",
"accounts": "comptes", "accounts": "comptes",
"changeOwnerButton": "Change Owner" "changeOwnerButton": "Change Owner",
"allocatedLicense": "Licences attribuées",
"search": "Recherche"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -388,17 +398,18 @@
"title": "Licence", "title": "Licence",
"orderHistory": "Historique des commandes", "orderHistory": "Historique des commandes",
"orderDate": "Date de commande", "orderDate": "Date de commande",
"issueDate": "Date de émission", "issueDate": "Date de délivrance",
"numberOfOrder": "Nombre de licences commandées", "numberOfOrder": "Nombre de licences commandées",
"poNumber": "Numéro de bon de commande", "poNumber": "Numéro de bon de commande",
"status": "État", "status": "État",
"issueRequesting": "Licences en commande", "issueRequesting": "Licences en commande",
"issued": "Licence issued", "issued": "Licence délivrée",
"orderCanceled": "Commande annulée", "orderCanceled": "Commande annulée",
"issue": "Problème", "issue": "Délivrer",
"issueCancel": "Annuler les licences", "issueCancel": "Annuler les licences",
"orderCancel": "Annuler commande", "orderCancel": "Annuler commande",
"histories": "histoires" "histories": "histoires",
"licenseType": "Type de licence"
}, },
"message": { "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.", "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.",
@ -617,7 +628,16 @@
"label": { "label": {
"title": "Support", "title": "Support",
"howToUse": "Comment utiliser le système", "howToUse": "Comment utiliser le système",
"supportPageLink": "Guide de l'utilisateur du cloud OMDS" "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.",
"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."
}, },
"text": { "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." "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."
@ -653,5 +673,22 @@
"upperLayerId": "Upper Layer ID", "upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower 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,9 +39,6 @@ RUN mkdir -p /tmp/gotools \
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \ && mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
&& rm -rf /tmp/gotools && rm -rf /tmp/gotools
# Update NPM
RUN npm install -g npm
# 以下 ユーザー権限で実施 # 以下 ユーザー権限で実施
USER $USERNAME USER $USERNAME
# copy init-script # copy init-script

View File

@ -106,6 +106,7 @@ export const LICENSE_TYPE = {
TRIAL: "TRIAL", TRIAL: "TRIAL",
NORMAL: "NORMAL", NORMAL: "NORMAL",
CARD: "CARD", CARD: "CARD",
NONE: "NONE",
} as const; } as const;
/** /**
* *
@ -350,6 +351,7 @@ export const FILE_RETENTION_DAYS_DEFAULT = 30;
*/ */
export const LICENSE_COUNT_ANALYSIS_HEADER = { export const LICENSE_COUNT_ANALYSIS_HEADER = {
ACCOUNT: "アカウント", ACCOUNT: "アカウント",
COUNTRY:"国",
TARGET_YEAE_AND_MONTH: "対象年月", TARGET_YEAE_AND_MONTH: "対象年月",
CATEGORY_1: "カテゴリー1", CATEGORY_1: "カテゴリー1",
CATEGORY_2: "カテゴリー2", CATEGORY_2: "カテゴリー2",
@ -385,6 +387,7 @@ export const LICENSE_COUNT_ANALYSIS_LICENSE_TYPE = {
CARD: "Card", CARD: "Card",
SWITCH_FROM_TRIAL: "トライアルから切り替え", SWITCH_FROM_TRIAL: "トライアルから切り替え",
SWITCH_FROM_CARD: "カードから切り替え", SWITCH_FROM_CARD: "カードから切り替え",
NONE: "移行ライセンス",
}; };
/** /**
* CSV項目で使用する日本語 * CSV項目で使用する日本語

View File

@ -435,6 +435,7 @@ export async function transferData(
// 出力データのヘッダーを作成 // 出力データのヘッダーを作成
const header = [ const header = [
'"' + LICENSE_COUNT_ANALYSIS_HEADER.ACCOUNT + '",', '"' + LICENSE_COUNT_ANALYSIS_HEADER.ACCOUNT + '",',
'"' + LICENSE_COUNT_ANALYSIS_HEADER.COUNTRY + '",',
'"' + LICENSE_COUNT_ANALYSIS_HEADER.TARGET_YEAE_AND_MONTH + '",', '"' + LICENSE_COUNT_ANALYSIS_HEADER.TARGET_YEAE_AND_MONTH + '",',
'"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_1 + '",', '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_1 + '",',
'"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_2 + '",', '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_2 + '",',
@ -486,6 +487,7 @@ export async function transferData(
(license) => license.account_id === account.id (license) => license.account_id === account.id
); );
// 抽出したライセンスを種別ごとに分ける。typeカラムで判別トライアル・通常・カード // 抽出したライセンスを種別ごとに分ける。typeカラムで判別トライアル・通常・カード
// 種別にNoneを追加⇒旧システムからの移行分の状況も確認したい 2024年7月9日
const trialLicenses = accountLicenses.filter( const trialLicenses = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.TRIAL (license) => license.type === LICENSE_TYPE.TRIAL
); );
@ -495,6 +497,9 @@ export async function transferData(
const cardLicenses = accountLicenses.filter( const cardLicenses = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.CARD (license) => license.type === LICENSE_TYPE.CARD
); );
const noneLicense = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.NONE
);
// 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated
const usedTrialLicenses = trialLicenses.filter( const usedTrialLicenses = trialLicenses.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
@ -505,6 +510,9 @@ export async function transferData(
const usedCardLicenses = cardLicenses.filter( const usedCardLicenses = cardLicenses.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
); );
const usedNoneLicense = noneLicense.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
);
// どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。
// allcated_user_idからユーザーを特定 // allcated_user_idからユーザーを特定
// (Author・Typist・None) // (Author・Typist・None)
@ -584,6 +592,7 @@ export async function transferData(
const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length;
const usedCardLicensesTypistCount = usedCardLicensesTypist.length; const usedCardLicensesTypistCount = usedCardLicensesTypist.length;
const usedCardLicensesNoneCount = usedCardLicensesNone.length; const usedCardLicensesNoneCount = usedCardLicensesNone.length;
const usedNoneLicenseCount = usedNoneLicense.length;
// アカウントに紐づく当月発行ライセンスを取得 // アカウントに紐づく当月発行ライセンスを取得
const accountCurrentMonthIssuedLicenses = const accountCurrentMonthIssuedLicenses =
@ -813,10 +822,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.company_name, account.company_name,
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -854,10 +865,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.company_name, account.company_name,
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -895,10 +908,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.company_name, account.company_name,
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -959,6 +974,7 @@ export async function transferData(
(license) => license.account_id === account.id (license) => license.account_id === account.id
); );
// 抽出したライセンスを種別ごとに分ける。typeカラムで判別トライアル・通常・カード // 抽出したライセンスを種別ごとに分ける。typeカラムで判別トライアル・通常・カード
// 種別にNoneを追加⇒旧システムからの移行分の状況も確認したい 2024年7月9日
const trialLicenses = accountLicenses.filter( const trialLicenses = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.TRIAL (license) => license.type === LICENSE_TYPE.TRIAL
); );
@ -968,6 +984,9 @@ export async function transferData(
const cardLicenses = accountLicenses.filter( const cardLicenses = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.CARD (license) => license.type === LICENSE_TYPE.CARD
); );
const noneLicense = accountLicenses.filter(
(license) => license.type === LICENSE_TYPE.NONE
);
// 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated
const usedTrialLicenses = trialLicenses.filter( const usedTrialLicenses = trialLicenses.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
@ -978,6 +997,9 @@ export async function transferData(
const usedCardLicenses = cardLicenses.filter( const usedCardLicenses = cardLicenses.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
); );
const usedNoneLicense = noneLicense.filter(
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
);
// どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。
// allcated_user_idからユーザーを特定 // allcated_user_idからユーザーを特定
// (Author・Typist・None) // (Author・Typist・None)
@ -1048,6 +1070,7 @@ export async function transferData(
const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length;
const usedCardLicensesTypistCount = usedCardLicensesTypist.length; const usedCardLicensesTypistCount = usedCardLicensesTypist.length;
const usedCardLicensesNoneCount = usedCardLicensesNone.length; const usedCardLicensesNoneCount = usedCardLicensesNone.length;
const usedNoneLicenseCount = usedNoneLicense.length;
// アカウントに紐づく当月発行ライセンスを取得 // アカウントに紐づく当月発行ライセンスを取得
const accountCurrentMonthIssuedLicenses = const accountCurrentMonthIssuedLicenses =
@ -1252,10 +1275,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.id.toString(), account.id.toString(),
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -1293,10 +1318,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.id.toString(), account.id.toString(),
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -1334,10 +1361,12 @@ export async function transferData(
await createOutputData( await createOutputData(
context, context,
account.id.toString(), account.id.toString(),
account.country,
targetMonthYYYYMM, targetMonthYYYYMM,
trialLicensesCount, trialLicensesCount,
normalLicensesCount, normalLicensesCount,
cardLicensesCount, cardLicensesCount,
usedNoneLicenseCount,
usedTrialLicensesAuthorCount, usedTrialLicensesAuthorCount,
usedTrialLicensesTypistCount, usedTrialLicensesTypistCount,
usedTrialLicensesNoneCount, usedTrialLicensesNoneCount,
@ -1429,10 +1458,12 @@ export async function transferData(
async function createOutputData( async function createOutputData(
context: InvocationContext, context: InvocationContext,
company_name: string, company_name: string,
country: string,
targetMonthYYYYMM: string, targetMonthYYYYMM: string,
trialLicensesCount: number, trialLicensesCount: number,
normalLicensesCount: number, normalLicensesCount: number,
cardLicensesCount: number, cardLicensesCount: number,
usedNoneLicenseCount: number,
usedTrialLicensesAuthorCount: number, usedTrialLicensesAuthorCount: number,
usedTrialLicensesTypistCount: number, usedTrialLicensesTypistCount: number,
usedTrialLicensesNoneCount: number, usedTrialLicensesNoneCount: number,
@ -1471,6 +1502,8 @@ async function createOutputData(
resultOutputData.push( resultOutputData.push(
// 会社名(ダブルクォーテーションで囲む) // 会社名(ダブルクォーテーションで囲む)
'"' + company_name + '",', '"' + company_name + '",',
// 国
'"' + country + '",',
// 対象年月先月YYYYMM // 対象年月先月YYYYMM
// 2024年3月に実行した場合202402 // 2024年3月に実行した場合202402
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
@ -1488,6 +1521,7 @@ async function createOutputData(
// アカウントが保持する通常ライセンス[] // アカウントが保持する通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",',
@ -1498,6 +1532,7 @@ async function createOutputData(
// アカウントが保持するカードライセンス[] // アカウントが保持するカードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",',
@ -1505,9 +1540,21 @@ async function createOutputData(
'"' + "" + '",', '"' + "" + '",',
'"' + cardLicensesCount.toString() + '"\r\n' '"' + cardLicensesCount.toString() + '"\r\n'
); );
// ユーザーが使用中の移行ライセンス
resultOutputData.push(
'"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.NONE + '",',
'"' + "" + '",',
'"' + usedNoneLicenseCount.toString() + '"\r\n'
);
// Authorが使用中のトライアルライセンス[] // Authorが使用中のトライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1518,6 +1565,7 @@ async function createOutputData(
// Typistが使用中のトライアルライセンス[] // Typistが使用中のトライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1528,6 +1576,7 @@ async function createOutputData(
// Noneが使用中のトライアルライセンス[] // Noneが使用中のトライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1538,6 +1587,7 @@ async function createOutputData(
// Authorが使用中の通常ライセンス[] // Authorが使用中の通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1548,6 +1598,7 @@ async function createOutputData(
// Typistが使用中の通常ライセンス[] // Typistが使用中の通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1558,6 +1609,7 @@ async function createOutputData(
// Noneが使用中の通常ライセンス[] // Noneが使用中の通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1568,6 +1620,7 @@ async function createOutputData(
// Authorが使用中のカードライセンス[] // Authorが使用中のカードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1578,6 +1631,7 @@ async function createOutputData(
// Typistが使用中のカードライセンス[] // Typistが使用中のカードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1588,6 +1642,7 @@ async function createOutputData(
// Noneが使用中のカードライセンス[] // Noneが使用中のカードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
@ -1598,6 +1653,7 @@ async function createOutputData(
// アカウントが保持する当月発行トライアルライセンス[] // アカウントが保持する当月発行トライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1608,6 +1664,7 @@ async function createOutputData(
// アカウントが保持する当月発行通常ライセンス[] // アカウントが保持する当月発行通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1618,6 +1675,7 @@ async function createOutputData(
// アカウントが保持する当月発行カードライセンス[] // アカウントが保持する当月発行カードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1628,6 +1686,7 @@ async function createOutputData(
// Authorに割り当てられたままの失効トライアルライセンス[] // Authorに割り当てられたままの失効トライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1638,6 +1697,7 @@ async function createOutputData(
// Typistに割り当てられたままの失効トライアルライセンス[] // Typistに割り当てられたままの失効トライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1648,6 +1708,7 @@ async function createOutputData(
// Noneに割り当てられたままの失効トライアルライセンス[] // Noneに割り当てられたままの失効トライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1658,6 +1719,7 @@ async function createOutputData(
// 未割当の失効トライアルライセンス[] // 未割当の失効トライアルライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1668,6 +1730,7 @@ async function createOutputData(
// Authorに割り当てられたままの失効通常ライセンス[] // Authorに割り当てられたままの失効通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1678,6 +1741,7 @@ async function createOutputData(
// Typistに割り当てられたままの失効通常ライセンス[] // Typistに割り当てられたままの失効通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1688,6 +1752,7 @@ async function createOutputData(
// Noneに割り当てられたままの失効通常ライセンス[] // Noneに割り当てられたままの失効通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1698,6 +1763,7 @@ async function createOutputData(
// 未割当の失効通常ライセンス[] // 未割当の失効通常ライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1708,6 +1774,7 @@ async function createOutputData(
// Authorに割り当てられたままの失効カードライセンス[] // Authorに割り当てられたままの失効カードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1718,6 +1785,7 @@ async function createOutputData(
// Typistに割り当てられたままの失効カードライセンス[] // Typistに割り当てられたままの失効カードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1728,6 +1796,7 @@ async function createOutputData(
// Noneに割り当てられたままの失効カードライセンス[] // Noneに割り当てられたままの失効カードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1738,6 +1807,7 @@ async function createOutputData(
// 未割当の失効カードライセンス[] // 未割当の失効カードライセンス[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1748,6 +1818,7 @@ async function createOutputData(
// Authorにトライアルライセンスからの切り替え[] // Authorにトライアルライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1758,6 +1829,7 @@ async function createOutputData(
// Typistにトライアルライセンスからの切り替え[] // Typistにトライアルライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1768,6 +1840,7 @@ async function createOutputData(
// Noneにトライアルライセンスからの切り替え[] // Noneにトライアルライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1778,6 +1851,7 @@ async function createOutputData(
// Authorにカードライセンスからの切り替え[] // Authorにカードライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1788,6 +1862,7 @@ async function createOutputData(
// Typistにカードライセンスからの切り替え[] // Typistにカードライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',
@ -1798,6 +1873,7 @@ async function createOutputData(
// Noneにカードライセンスからの切り替え[] // Noneにカードライセンスからの切り替え[]
resultOutputData.push( resultOutputData.push(
'"' + company_name + '",', '"' + company_name + '",',
'"' + country + '",',
'"' + targetMonthYYYYMM + '",', '"' + targetMonthYYYYMM + '",',
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
'"' + "" + '",', '"' + "" + '",',

View File

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

View File

@ -0,0 +1,133 @@
import { SendGridService } from "./sendgrid";
import sendgrid from "@sendgrid/mail";
import { InvocationContext } from "@azure/functions";
// sendgridのsend関数をモック化
jest.mock("@sendgrid/mail", () => ({
send: jest.fn(),
setApiKey: jest.fn(),
}));
describe("SendGridService", () => {
let service: SendGridService;
let mockContext: Partial<InvocationContext>;
beforeEach(() => {
process.env.SENDGRID_API_KEY = "dummy_key"; // 必要な環境変数
service = new SendGridService(); // SendGridServiceのインスタンスを作成
mockContext = {
log: jest.fn(),
warn: jest.fn(),
}; // InvocationContextのモック
});
afterEach(() => {
jest.clearAllMocks();
});
it("should send an email with no duplicate to and cc addresses", async () => {
// モックデータ
const to = ["test1@example.com", "test2@example.com"];
const cc = ["test3@example.com"];
const from = "sender@example.com";
const subject = "Test Subject";
const text = "Test Text";
const html = "<p>Test HTML</p>";
// sendgrid.sendのモック
(sendgrid.send as jest.Mock).mockResolvedValue([
{ statusCode: 202, body: "OK" },
]);
// メール送信を実行
await service.sendMail(
mockContext as InvocationContext,
to,
cc,
from,
subject,
text,
html
);
// sendgrid.sendが呼ばれたことを確認
expect(sendgrid.send).toHaveBeenCalledWith({
from: { email: from },
to: to.map((v) => ({ email: v })),
cc: cc.map((v) => ({ email: v })),
subject,
text,
html,
});
// ログが出力されているか確認
expect(mockContext.log).toHaveBeenCalledWith(`[IN] sendMail`);
expect(mockContext.log).toHaveBeenCalledWith(`[OUT] sendMail`);
});
it("should remove duplicate addresses in to and cc", async () => {
const to = ["test1@example.com", "test2@example.com", "test1@example.com"]; // 重複あり
const cc = ["test2@example.com", "test3@example.com"]; // 重複あり
const from = "sender@example.com";
const subject = "Test Subject";
const text = "Test Text";
const html = "<p>Test HTML</p>";
// sendgrid.sendのモック
(sendgrid.send as jest.Mock).mockResolvedValue([
{ statusCode: 202, body: "OK" },
]);
// メール送信を実行
await service.sendMail(
mockContext as InvocationContext,
to,
cc,
from,
subject,
text,
html
);
// 重複が削除されているか確認
expect(sendgrid.send).toHaveBeenCalledWith({
from: { email: from },
to: [{ email: "test1@example.com" }, { email: "test2@example.com" }], // 重複削除後
cc: [{ email: "test3@example.com" }], // ccから重複が削除される
subject,
text,
html,
});
});
it("should log an error when send fails", async () => {
const to = ["test1@example.com"];
const cc = ["test2@example.com"];
const from = "sender@example.com";
const subject = "Test Subject";
const text = "Test Text";
const html = "<p>Test HTML</p>";
// sendgrid.sendがエラーを投げるモック
(sendgrid.send as jest.Mock).mockRejectedValue(new Error("Send failed"));
// エラーが投げられるか確認
await expect(
service.sendMail(
mockContext as InvocationContext,
to,
cc,
from,
subject,
text,
html
)
).rejects.toThrow("Send failed");
// エラーログが出力されているか確認
expect(mockContext.warn).toHaveBeenCalledWith("send mail faild.");
expect(mockContext.warn).toHaveBeenCalledWith(
expect.stringContaining("sendMail error=Error: Send failed")
);
});
});

View File

@ -32,22 +32,28 @@ export class SendGridService {
): Promise<void> { ): Promise<void> {
context.log(`[IN] ${this.sendMail.name}`); context.log(`[IN] ${this.sendMail.name}`);
try { try {
// 1. toの重複を削除
const uniqueTo = [...new Set(to)];
// 2. ccの重複を削除
let uniqueCc = [...new Set(cc)];
// 3. toとccの重複を削除cc側から削除
uniqueCc = uniqueCc.filter((email) => !uniqueTo.includes(email));
const res = await sendgrid const res = await sendgrid
.send({ .send({
from: { from: {
email: from, email: from,
}, },
to: to.map((v) => ({ email: v })), to: uniqueTo.map((v) => ({ email: v })),
cc: cc.map((v) => ({ email: v })), cc: uniqueCc.map((v) => ({ email: v })),
subject: subject, subject: subject,
text: text, text: text,
html: html, html: html,
}) })
.then((v) => v[0]); .then((v) => v[0]);
context.log( context.log(
` status code: ${ ` status code: ${res.statusCode} body: ${JSON.stringify(res.body)}`
res.statusCode
} body: ${JSON.stringify(res.body)}`,
); );
} catch (e) { } catch (e) {
context.warn(`send mail faild.`); context.warn(`send mail faild.`);

View File

@ -23,10 +23,6 @@ import {
import { InvocationContext } from "@azure/functions"; import { InvocationContext } from "@azure/functions";
import { import {
LICENSE_ALLOCATED_STATUS, LICENSE_ALLOCATED_STATUS,
LICENSE_COUNT_ANALYSIS_CATEGORY_1,
LICENSE_COUNT_ANALYSIS_CATEGORY_2,
LICENSE_COUNT_ANALYSIS_LICENSE_TYPE,
LICENSE_COUNT_ANALYSIS_ROLE,
SWITCH_FROM_TYPE, SWITCH_FROM_TYPE,
} from "../constants"; } from "../constants";
import { BlobstorageService } from "../blobstorage/blobstorage.service"; import { BlobstorageService } from "../blobstorage/blobstorage.service";
@ -305,6 +301,35 @@ describe("analysisLicenses", () => {
last2Month last2Month
); );
// 有効な移行ライセンスの作成
await createLicense(
source,
26,
expiringSoonDate,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
null,
null,
null,
last2Month
);
// 期限切れの移行ライセンスの作成
await createLicense(
source,
27,
lastMonth,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
null,
null,
null,
last2Month
);
// 第五階層がその月におこなったライセンス切り替え情報を作成 // 第五階層がその月におこなったライセンス切り替え情報を作成
// 条件: // 条件:
// ・第五アカウント // ・第五アカウント
@ -379,23 +404,25 @@ describe("analysisLicenses", () => {
throw new Error("ユーザー取得できていないので失敗"); throw new Error("ユーザー取得できていないので失敗");
} }
expect(result.avairableLicenses).toHaveLength(6); expect(result.avairableLicenses).toHaveLength(7);
expect(result.avairableLicenses[0].id).toBe(1); expect(result.avairableLicenses[0].id).toBe(1);
expect(result.avairableLicenses[1].id).toBe(2); expect(result.avairableLicenses[1].id).toBe(2);
expect(result.avairableLicenses[2].id).toBe(3); expect(result.avairableLicenses[2].id).toBe(3);
expect(result.avairableLicenses[3].id).toBe(11); expect(result.avairableLicenses[3].id).toBe(11);
expect(result.avairableLicenses[4].id).toBe(12); expect(result.avairableLicenses[4].id).toBe(12);
expect(result.avairableLicenses[5].id).toBe(13); expect(result.avairableLicenses[5].id).toBe(13);
expect(result.avairableLicenses[6].id).toBe(26);
expect(result.licensesIssuedInTargetMonth).toHaveLength(3); expect(result.licensesIssuedInTargetMonth).toHaveLength(3);
expect(result.licensesIssuedInTargetMonth[0].id).toBe(11); expect(result.licensesIssuedInTargetMonth[0].id).toBe(11);
expect(result.licensesIssuedInTargetMonth[1].id).toBe(12); expect(result.licensesIssuedInTargetMonth[1].id).toBe(12);
expect(result.licensesIssuedInTargetMonth[2].id).toBe(13); expect(result.licensesIssuedInTargetMonth[2].id).toBe(13);
expect(result.licensesExpiredInTargetMonth).toHaveLength(3); expect(result.licensesExpiredInTargetMonth).toHaveLength(4);
expect(result.licensesExpiredInTargetMonth[0].id).toBe(21); expect(result.licensesExpiredInTargetMonth[0].id).toBe(21);
expect(result.licensesExpiredInTargetMonth[1].id).toBe(22); expect(result.licensesExpiredInTargetMonth[1].id).toBe(22);
expect(result.licensesExpiredInTargetMonth[2].id).toBe(23); expect(result.licensesExpiredInTargetMonth[2].id).toBe(23);
expect(result.licensesExpiredInTargetMonth[3].id).toBe(27);
expect(result.switchedlicensesInTargetMonth).toHaveLength(2); expect(result.switchedlicensesInTargetMonth).toHaveLength(2);
expect(result.switchedlicensesInTargetMonth[0].id).toBe(1); expect(result.switchedlicensesInTargetMonth[0].id).toBe(1);
@ -647,6 +674,35 @@ describe("analysisLicenses", () => {
last2Month last2Month
); );
// 有効な移行ライセンスの作成
await createLicenseArchive(
source,
26,
null,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
last2Month
);
// 期限切れの移行ライセンスの作成
await createLicenseArchive(
source,
27,
lastMonth,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
last2Month
);
// 第五階層がその月におこなったライセンス切り替え情報を作成 // 第五階層がその月におこなったライセンス切り替え情報を作成
// 条件: // 条件:
// ・第五アカウント // ・第五アカウント
@ -722,23 +778,25 @@ describe("analysisLicenses", () => {
); );
} }
expect(result.deletedAvairableLicenses).toHaveLength(6); expect(result.deletedAvairableLicenses).toHaveLength(7);
expect(result.deletedAvairableLicenses[0].id).toBe(1); expect(result.deletedAvairableLicenses[0].id).toBe(1);
expect(result.deletedAvairableLicenses[1].id).toBe(2); expect(result.deletedAvairableLicenses[1].id).toBe(2);
expect(result.deletedAvairableLicenses[2].id).toBe(3); expect(result.deletedAvairableLicenses[2].id).toBe(3);
expect(result.deletedAvairableLicenses[3].id).toBe(11); expect(result.deletedAvairableLicenses[3].id).toBe(11);
expect(result.deletedAvairableLicenses[4].id).toBe(12); expect(result.deletedAvairableLicenses[4].id).toBe(12);
expect(result.deletedAvairableLicenses[5].id).toBe(13); expect(result.deletedAvairableLicenses[5].id).toBe(13);
expect(result.deletedAvairableLicenses[6].id).toBe(26);
expect(result.deletedLicensesIssuedInTargetMonth).toHaveLength(3); expect(result.deletedLicensesIssuedInTargetMonth).toHaveLength(3);
expect(result.deletedLicensesIssuedInTargetMonth[0].id).toBe(11); expect(result.deletedLicensesIssuedInTargetMonth[0].id).toBe(11);
expect(result.deletedLicensesIssuedInTargetMonth[1].id).toBe(12); expect(result.deletedLicensesIssuedInTargetMonth[1].id).toBe(12);
expect(result.deletedLicensesIssuedInTargetMonth[2].id).toBe(13); expect(result.deletedLicensesIssuedInTargetMonth[2].id).toBe(13);
expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(3); expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(4);
expect(result.deletedLicensesExpiredInTargetMonth[0].id).toBe(21); expect(result.deletedLicensesExpiredInTargetMonth[0].id).toBe(21);
expect(result.deletedLicensesExpiredInTargetMonth[1].id).toBe(22); expect(result.deletedLicensesExpiredInTargetMonth[1].id).toBe(22);
expect(result.deletedLicensesExpiredInTargetMonth[2].id).toBe(23); expect(result.deletedLicensesExpiredInTargetMonth[2].id).toBe(23);
expect(result.deletedLicensesExpiredInTargetMonth[3].id).toBe(27);
expect(result.deletedSwitchedlicensesInTargetMonth).toHaveLength(2); expect(result.deletedSwitchedlicensesInTargetMonth).toHaveLength(2);
expect(result.deletedSwitchedlicensesInTargetMonth[0].id).toBe(1); expect(result.deletedSwitchedlicensesInTargetMonth[0].id).toBe(1);
@ -1682,6 +1740,62 @@ describe("analysisLicenses", () => {
SWITCH_FROM_TYPE.CARD SWITCH_FROM_TYPE.CARD
); );
// 移行ライセンス用のデータを作成
// 移行ライセンスを持つ生きているアカウントのユーザーの情報を作成する
const activeUserForTransitionData1 = await makeTestUser(source, {
account_id: account5_1.id,
role: "author",
});
const activeUserForTransitionData2 = await makeTestUser(source, {
account_id: account5_1.id,
role: "author",
});
// ユーザーに紐づく有効な移行ライセンスを作成
await createLicense(
source,
56,
expiringSoonDate,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
activeUserForTransitionData1.id,
null,
null,
null,
last2Month
);
// ユーザーに紐づく期限切れの移行ライセンスを作成
// これは集計しないのでCSVには出力されない
await createLicense(
source,
57,
lastMonth,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
activeUserForTransitionData2.id,
null,
null,
null,
last2Month
);
// 誰にも紐づかない移行ライセンスを作成
// これは集計しないのでCSVには出力されない
await createLicense(
source,
58,
expiringSoonDate,
account5_1.id,
"NONE",
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
last2Month
);
const result = await getBaseData(context, lastMonthYYYYMM, source); const result = await getBaseData(context, lastMonthYYYYMM, source);
// 削除されたアカウントとユーザーの情報を作成する // 削除されたアカウントとユーザーの情報を作成する
@ -1690,6 +1804,7 @@ describe("analysisLicenses", () => {
{ {
tier: 5, tier: 5,
parent_account_id: account4.id, parent_account_id: account4.id,
country: "CA",
}, },
{ {
external_id: "external_id_tier5admin1", external_id: "external_id_tier5admin1",
@ -2585,6 +2700,62 @@ describe("analysisLicenses", () => {
lastMonth, lastMonth,
SWITCH_FROM_TYPE.CARD SWITCH_FROM_TYPE.CARD
); );
// 移行ライセンス用のデータを作成
// 移行ライセンスを持つ削除アカウントのユーザーの情報を作成する
const DeletedUserForTransitionData1 = await makeTestUserArchive(source, {
account_id: account5_1_D.id,
role: "author",
});
const DeletedUserForTransitionData2 = await makeTestUserArchive(source, {
account_id: account5_1_D.id,
role: "author",
});
// 移行ライセンスを生きているアカウントに作成
await createLicenseArchive(
source,
56,
expiringSoonDate,
account5_1_D.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
DeletedUserForTransitionData1.id,
null,
null,
null,
last2Month
);
// ユーザーに紐づく期限切れの移行ライセンスを作成
// これは集計しないのでCSVには出力されない
await createLicenseArchive(
source,
57,
lastMonth,
account5_1_D.id,
"NONE",
LICENSE_ALLOCATED_STATUS.ALLOCATED,
DeletedUserForTransitionData2.id,
null,
null,
null,
last2Month
);
// 誰にも紐づかない移行ライセンスを作成
// これは集計しないのでCSVには出力されない
await createLicenseArchive(
source,
58,
expiringSoonDate,
account5_1_D.id,
"NONE",
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
last2Month
);
const result_D = await getBaseDataFromDeletedAccounts( const result_D = await getBaseDataFromDeletedAccounts(
context, context,
lastMonthYYYYMM, lastMonthYYYYMM,
@ -2602,139 +2773,143 @@ describe("analysisLicenses", () => {
csvContentUS += transferDataResult.outputDataUS[i]; csvContentUS += transferDataResult.outputDataUS[i];
} }
expect(csvContentUS).toBe( expect(csvContentUS).toBe(
'"アカウント","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' + '"アカウント","国","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
`\r\n` +
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","移行ライセンス","","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + `"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + `"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + `"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + `"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
"\r\n" + "\r\n" +
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + `"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","移行ライセンス","","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + `"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + `"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + `"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + `"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
"\r\n" + "\r\n" +
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + `"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
"\r\n" +
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
"\r\n" "\r\n"
); );
}); });

View File

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

View File

@ -2,5 +2,6 @@ DB_HOST=omds-mysql
DB_PORT=3306 DB_PORT=3306
DB_NAME=omds DB_NAME=omds
DB_NAME_CCB=omds_ccb DB_NAME_CCB=omds_ccb
DB_NAME_PH1ENHANCE=omds_ph1enhance
DB_USERNAME=omdsdbuser DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass DB_PASSWORD=omdsdbpass

View File

@ -6,6 +6,10 @@ ccb:
dialect: mysql dialect: mysql
dir: /app/dictation_server/db/migrations dir: /app/dictation_server/db/migrations
datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME_CCB}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME_CCB}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true
ph1_enhance:
dialect: mysql
dir: /app/dictation_server/db/migrations
datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME_PH1ENHANCE}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true
ci: ci:
dialect: mysql dialect: mysql
dir: ./dictation_server/db/migrations dir: ./dictation_server/db/migrations

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE `license_orders` ADD COLUMN `type` VARCHAR(255) DEFAULT "NORMAL" COMMENT 'ライセンス種別' AFTER `issued_at`;
-- +migrate Down
ALTER TABLE `license_orders` DROP COLUMN `type`;

View File

@ -0,0 +1,16 @@
-- +migrate Up
CREATE TABLE IF NOT EXISTS `task_filters` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'タスク検索条件ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT 'ユーザーID',
`author_id` VARCHAR(255) COMMENT '検索キー:AuthorID',
`file_name` VARCHAR(1024) COMMENT '検索キー:ファイル名',
`deleted_at` TIMESTAMP COMMENT '削除時刻',
`created_by` VARCHAR(255) COMMENT '作成者',
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
`updated_by` VARCHAR(255) COMMENT '更新者',
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
INDEX `idx_task_filters_user_id` (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-- +migrate Down
DROP TABLE `task_filters`;

View File

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

View File

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

View File

@ -1417,6 +1417,119 @@
"security": [{ "bearer": [] }] "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": { "/accounts/me/file-delete-setting": {
"post": { "post": {
"operationId": "updateFileDeleteSetting", "operationId": "updateFileDeleteSetting",
@ -1968,11 +2081,76 @@
"tags": ["users"] "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": { "/users": {
"get": { "get": {
"operationId": "getUsers", "operationId": "getUsers",
"summary": "", "summary": "",
"parameters": [], "parameters": [
{
"name": "userName",
"required": false,
"in": "query",
"schema": { "type": "string" }
},
{
"name": "email",
"required": false,
"in": "query",
"schema": { "type": "string" }
}
],
"responses": { "responses": {
"200": { "200": {
"description": "成功時のレスポンス", "description": "成功時のレスポンス",
@ -2184,6 +2362,106 @@
"security": [{ "bearer": [] }] "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": { "/users/update": {
"post": { "post": {
"operationId": "updateUser", "operationId": "updateUser",
@ -2996,6 +3274,20 @@
"in": "query", "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", "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" } "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": { "responses": {
@ -3524,6 +3816,68 @@
"security": [{ "bearer": [] }] "security": [{ "bearer": [] }]
} }
}, },
"/tasks/{audioFileId}/reopen": {
"post": {
"operationId": "reopen",
"summary": "",
"description": "完了した文字起こしタスクを再開しますステータスをPendingにします",
"parameters": [
{
"name": "audioFileId",
"required": true,
"in": "path",
"description": "ODMS Cloud上の音声ファイルID",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeStatusResponse"
}
}
}
},
"400": {
"description": "不正なパラメータ",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"404": {
"description": "指定したIDの音声ファイルが存在しない場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["tasks"],
"security": [{ "bearer": [] }]
}
},
"/licenses/orders": { "/licenses/orders": {
"post": { "post": {
"operationId": "createOrders", "operationId": "createOrders",
@ -3717,6 +4071,62 @@
"security": [{ "bearer": [] }] "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": { "/licenses/orders/cancel": {
"post": { "post": {
"operationId": "cancelOrder", "operationId": "cancelOrder",
@ -4471,6 +4881,10 @@
"type": "number", "type": "number",
"description": "不足数({Stock license} - {Issue Requested}" "description": "不足数({Stock license} - {Issue Requested}"
}, },
"allocatedLicense": {
"type": "number",
"description": "有効期限内の割り当て済み総ライセンス数"
},
"issueRequesting": { "issueRequesting": {
"type": "number", "type": "number",
"description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数" "description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数"
@ -4483,6 +4897,7 @@
"stockLicense", "stockLicense",
"issuedRequested", "issuedRequested",
"shortage", "shortage",
"allocatedLicense",
"issueRequesting" "issueRequesting"
] ]
}, },
@ -4516,9 +4931,10 @@
"issueDate": { "type": "string", "description": "発行日付" }, "issueDate": { "type": "string", "description": "発行日付" },
"numberOfOrder": { "type": "number", "description": "注文数" }, "numberOfOrder": { "type": "number", "description": "注文数" },
"poNumber": { "type": "string", "description": "POナンバー" }, "poNumber": { "type": "string", "description": "POナンバー" },
"status": { "type": "string", "description": "注文状態" } "status": { "type": "string", "description": "注文状態" },
"type": { "type": "string", "description": "ライセンス種別" }
}, },
"required": ["orderDate", "numberOfOrder", "poNumber", "status"] "required": ["orderDate", "numberOfOrder", "poNumber", "status", "type"]
}, },
"GetOrderHistoriesResponse": { "GetOrderHistoriesResponse": {
"type": "object", "type": "object",
@ -4731,6 +5147,60 @@
}, },
"required": ["total", "partners"] "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": { "UpdateAccountInfoRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4896,6 +5366,11 @@
"required": ["token"] "required": ["token"]
}, },
"ConfirmResponse": { "type": "object", "properties": {} }, "ConfirmResponse": { "type": "object", "properties": {} },
"ConfirmForceRequest": {
"type": "object",
"properties": { "userId": { "type": "number" } },
"required": ["userId"]
},
"User": { "User": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5063,6 +5538,34 @@
}, },
"required": ["direction", "paramName"] "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": { "PostUpdateUserRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5553,6 +6056,12 @@
}, },
"required": ["allocatableLicenses"] "required": ["allocatableLicenses"]
}, },
"IssueTrialLicenseRequest": {
"type": "object",
"properties": { "issuedAccount": { "type": "number" } },
"required": ["issuedAccount"]
},
"IssueTrialLicenseResponse": { "type": "object", "properties": {} },
"CancelOrderRequest": { "CancelOrderRequest": {
"type": "object", "type": "object",
"properties": { "poNumber": { "type": "string" } }, "properties": { "poNumber": { "type": "string" } },

View File

@ -41,6 +41,7 @@ import { LicensesController } from './features/licenses/licenses.controller';
import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module'; import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module';
import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module'; import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module';
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.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 { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module'; import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
import { TemplatesService } from './features/templates/templates.service'; import { TemplatesService } from './features/templates/templates.service';
@ -138,6 +139,7 @@ import { JobNumberRepositoryModule } from './repositories/job_number/job_number.
AuthGuardsModule, AuthGuardsModule,
SystemAccessGuardsModule, SystemAccessGuardsModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
WorktypesRepositoryModule, WorktypesRepositoryModule,
TermsModule, TermsModule,
RedisModule, RedisModule,

View File

@ -1,4 +1,5 @@
export type NotificationBody = { export type NotificationBody = {
id: string;
filename: string; filename: string;
authorId: string; authorId: string;
priority: string; priority: string;

View File

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

View File

@ -23,6 +23,7 @@ import { NotificationhubModule } from '../../gateways/notificationhub/notificati
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { AuthGuardsModule } from '../../common/guards/auth/authguards.module'; import { AuthGuardsModule } from '../../common/guards/auth/authguards.module';
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.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 { AuthService } from '../../features/auth/auth.service';
import { AccountsService } from '../../features/accounts/accounts.service'; import { AccountsService } from '../../features/accounts/accounts.service';
import { UsersService } from '../../features/users/users.service'; import { UsersService } from '../../features/users/users.service';
@ -78,6 +79,7 @@ export const makeTestingModule = async (
BlobstorageModule, BlobstorageModule,
AuthGuardsModule, AuthGuardsModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
WorktypesRepositoryModule, WorktypesRepositoryModule,
TermsRepositoryModule, TermsRepositoryModule,
JobNumberRepositoryModule, JobNumberRepositoryModule,

View File

@ -10,6 +10,7 @@ import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.servi
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { Account } from '../../repositories/accounts/entity/account.entity'; import { Account } from '../../repositories/accounts/entity/account.entity';
import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { SearchPartnerInfoFromDb } from '../../features/accounts/types/types';
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ### // ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
@ -279,6 +280,14 @@ export const overrideAccountsRepositoryService = <TService>(
) => Promise<{ newAccount: Account; adminUser: User }>; ) => Promise<{ newAccount: Account; adminUser: User }>;
deleteAccount?: (accountId: number, userId: number) => Promise<void>; deleteAccount?: (accountId: number, userId: number) => Promise<void>;
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>; 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 => { ): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得

View File

@ -12,6 +12,7 @@ import { AccountArchive } from '../../repositories/accounts/entity/account_archi
import { Task } from '../../repositories/tasks/entity/task.entity'; import { Task } from '../../repositories/tasks/entity/task.entity';
import { JobNumber } from '../../repositories/job_number/entity/job_number.entity'; import { JobNumber } from '../../repositories/job_number/entity/job_number.entity';
import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity'; import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity';
import { TaskFilters } from '../../repositories/task_filters/entity/task_filters.entity';
type InitialTestDBState = { type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[]; tier1Accounts: { account: Account; users: User[] }[];
@ -243,6 +244,9 @@ export const makeTestAccount = async (
// sort_criteriaテーブルにデータを追加 // sort_criteriaテーブルにデータを追加
await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC'); await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC');
// task_filtersテーブルにデータを追加
await createTaskFilter(datasource, userId, null, null);
// job_numberテーブルにデータを追加 // job_numberテーブルにデータを追加
await createJobNumber(datasource, accountId, '00000000'); await createJobNumber(datasource, accountId, '00000000');
@ -333,6 +337,10 @@ export const makeTestUser = async (
} }
// sort_criteriaテーブルにデータを追加 // sort_criteriaテーブルにデータを追加
await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC'); await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC');
// task_filtersテーブルにデータを追加
await createTaskFilter(datasource, user.id, null, null);
return user; return user;
}; };
@ -523,3 +531,46 @@ export const getSortCriteria = async (
}); });
return sortCriteria; 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

@ -28,6 +28,10 @@ export class EnvValidator {
@IsString() @IsString()
DB_NAME_CCB: string; DB_NAME_CCB: string;
@IsNotEmpty()
@IsString()
DB_NAME_PH1ENHANCE: string;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
DB_USERNAME: string; DB_USERNAME: string;

View File

@ -203,13 +203,6 @@ export const TASK_LIST_SORTABLE_ATTRIBUTES = [
*/ */
export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const; export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const;
/**
*
* NotificationHubの仕様上タグ式のOR条件で使えるタグは20個まで
* https://learn.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-tags-segment-push-message#tag-expressions
*/
export const TAG_MAX_COUNT = 20;
/** /**
* *
*/ */
@ -351,3 +344,9 @@ export const INITIAL_JOB_NUMBER = '00000000';
* @const {string} * @const {string}
*/ */
export const MAX_JOB_NUMBER = '99999999'; export const MAX_JOB_NUMBER = '99999999';
/**
*
* @const {number}
*/
export const ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY = 10;

View File

@ -85,6 +85,10 @@ import {
GetPartnerUsersRequest, GetPartnerUsersRequest,
UpdatePartnerInfoRequest, UpdatePartnerInfoRequest,
UpdatePartnerInfoResponse, UpdatePartnerInfoResponse,
SearchPartnersResponse,
SearchPartnersRequest,
GetPartnerHierarchyRequest,
GetPartnerHierarchyResponse,
} from './types/types'; } from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards'; import { AuthGuard } from '../../common/guards/auth/authguards';
@ -1885,6 +1889,187 @@ export class AccountsController {
return response; 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') @Post('/me')
@ApiResponse({ @ApiResponse({
status: HttpStatus.OK, status: HttpStatus.OK,

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,10 @@ import {
Partner, Partner,
GetCompanyNameResponse, GetCompanyNameResponse,
PartnerUser, PartnerUser,
SearchPartnersResponse,
GetPartnerHierarchyResponse,
SearchPartner,
PartnerHierarchy,
} from './types/types'; } from './types/types';
import { import {
DateWithZeroTime, DateWithZeroTime,
@ -981,13 +985,18 @@ export class AccountsService {
// 各子アカウントのShortageを算出してreturn用の変数にマージする // 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = []; const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) { for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
const { allocatableLicenseWithMargin, expiringSoonLicense } = const {
childPartnerLicenseFromRepository; allocatableLicenseWithMargin,
expiringSoonLicense,
allocatedLicense,
} = childPartnerLicenseFromRepository;
let childShortage = 0; let childShortage = 0;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) { if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
if ( if (
allocatableLicenseWithMargin === undefined || allocatableLicenseWithMargin === undefined ||
expiringSoonLicense === undefined expiringSoonLicense === undefined ||
allocatedLicense === undefined
) { ) {
throw new Error( throw new Error(
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`, `Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
@ -1008,6 +1017,9 @@ export class AccountsService {
{ {
shortage: childShortage, shortage: childShortage,
}, },
{
allocatedLicense: allocatedLicense,
},
); );
childrenPartnerLicenses.push(childPartnerLicense); childrenPartnerLicenses.push(childPartnerLicense);
@ -1072,6 +1084,7 @@ export class AccountsService {
orderDate: new Date(licenseOrder.ordered_at).toISOString(), orderDate: new Date(licenseOrder.ordered_at).toISOString(),
poNumber: licenseOrder.po_number, poNumber: licenseOrder.po_number,
status: licenseOrder.status, status: licenseOrder.status,
type: licenseOrder.type,
}; };
orderHistories.push(returnLicenseOrder); orderHistories.push(returnLicenseOrder);
} }
@ -2159,6 +2172,166 @@ export class AccountsService {
} }
} }
/**
*
* @param context
* @param externalId
* @param ownTier
* @param companyName
* @param targetAccountId
* @returns SearchPartnersResponse
*/
async searchPartners(
context: Context,
externalId: string,
ownTier: number,
companyName?: string,
targetAccountId?: number,
): Promise<SearchPartnersResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.searchPartners.name
} | params: { ` +
`externalId: ${externalId}, ` +
`ownTier: ${ownTier}, ` +
`companyName: ${companyName}, ` +
`targetAccountId: ${targetAccountId}, };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
const partnersRecords =
await this.accountRepository.getAccountsRelatedOwnAccount(
context,
accountId,
ownTier,
companyName,
targetAccountId,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
let externalIds = partnersRecords.map((x) => x.primaryAccountExternalId);
externalIds = externalIds.filter((item) => item !== undefined);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した情報とADB2Cから取得した情報をマージ
const searchResult = partnersRecords.map(
(dbuser): SearchPartner => {
const adb2cUser = adb2cUsers.find(
(adb2c) => dbuser.primaryAccountExternalId === adb2c.id,
);
if (!adb2cUser) {
throw new Error(
`adb2c user not found. externalId: ${dbuser.primaryAccountExternalId}`,
);
}
const { displayName: primaryAdmin, emailAddress: mail } =
getUserNameAndMailAddress(adb2cUser);
if (!mail) {
throw new Error(
`adb2c user mail not found. externalId: ${dbuser.primaryAccountExternalId}`,
);
}
return {
name: dbuser.name,
tier: dbuser.tier,
accountId: dbuser.accountId,
country: dbuser.country,
primaryAdmin: primaryAdmin,
email: mail,
};
},
);
return { searchResult };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.searchPartners.name}`,
);
}
}
/**
*
* @param context
* @param externalId
* @param ownTier
* @param targetAccountId
* @returns GetPartnersResponse
*/
async getPartnerHierarchy(
context: Context,
externalId: string,
ownTier: number,
targetAccountId: number,
): Promise<GetPartnerHierarchyResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getPartnerHierarchy.name
} | params: { ` +
`externalId: ${externalId}, ` +
`ownTier: ${ownTier}, ` +
`targetAccountId: ${targetAccountId}, };`,
);
try {
// 自身のアカウントIdを取得
const { account_id: ownAccountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// 対象の親アカウントを取得
const parentAccountIds = await this.accountRepository.getHierarchyParents(
context,
targetAccountId,
);
// 親アカウントの中に自身が存在しない場合、エラー
if (!parentAccountIds.includes(ownAccountId)) {
throw new Error(
`parent account not found. targetAccountId=${targetAccountId}, parentAccountIds=${parentAccountIds}`,
);
}
// 対象を含むアカウント階層をすべて取得
const targetAccountIds = [...parentAccountIds, targetAccountId];
const accounts = await this.accountRepository.findAccountsById(
context,
targetAccountIds,
);
const accountHierarchy = accounts
// 取得する階層を自身の階層までに限定
.filter((account) => account.tier >= ownTier)
.map((account): PartnerHierarchy => {
const { tier, id: accountId, company_name: name } = account;
return { tier, accountId, name };
})
// 上位の階層順になるようにソート
.sort((a, b) => a.tier - b.tier);
return { accountHierarchy };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getPartnerHierarchy.name}`,
);
}
}
/** /**
* *
* @param context * @param context

View File

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

View File

@ -4,6 +4,7 @@ import {
LicenseOrder, LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity'; } from '../../../repositories/licenses/entity/license.entity';
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.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 { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity'; import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
@ -21,6 +22,15 @@ export const getSortCriteriaList = async (dataSource: DataSource) => {
return await dataSource.getRepository(SortCriteria).find(); 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 ( export const createLicense = async (
datasource: DataSource, datasource: DataSource,
licenseId: number, licenseId: number,

View File

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

View File

@ -367,10 +367,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -472,10 +473,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -494,132 +496,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId);
}); });
it('タスク作成時に、自動ルーティングを行うことができるAPI実行者のAuthorIDとworkType', async () => { it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないIDが入っていた場合自動ルーティングを行うことができない', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー
const { author_id: authorAuthorId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// ルーティング先のタイピストのユーザー
const { id: typistUserId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
author_id: undefined,
});
// API実行者のユーザー
const { external_id: myExternalId, id: myUserId } = await makeTestUser(
source,
{
account_id: accountId,
external_id: 'my-author-user-external-id',
role: 'author',
author_id: 'MY_AUTHOR_ID',
},
);
// ワークタイプを作成
const { id: worktypeId, custom_worktype_id } = await createWorktype(
source,
accountId,
'worktypeId',
);
// テンプレートファイルを作成
const { id: templateFileId } = await createTemplateFile(
source,
accountId,
'templateFile',
'http://blob/url/templateFile.zip',
);
// ワークフローを作成
const { id: workflowId } = await createWorkflow(
source,
accountId,
myUserId, // API実行者のユーザーIDを設定
worktypeId,
templateFileId,
);
// ユーザーグループを作成
const { userGroupId } = await createUserGroupAndMember(
source,
accountId,
'userGroupName',
typistUserId, // ルーティング先のタイピストのユーザーIDを設定
);
// ワークフロータイピストを作成
await createWorkflowTypist(
source,
workflowId,
undefined,
userGroupId, // ルーティング先のユーザーグループIDを設定
);
// 初期値のジョブナンバーでjob_numberテーブルを作成
await createJobNumber(source, accountId, '00000000');
const blobParam = makeBlobstorageServiceMockValue();
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
const notificationHubService = module.get<NotificationhubService>(
NotificationhubService,
);
const result = await service.uploadFinished(
makeContext('trackingId', 'requestId'),
myExternalId, // API実行者のユーザーIDを設定
'http://blob/url/file.zip',
authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る
'file.zip',
'11:22:33',
'2023-05-26T11:22:33.444',
'2023-05-26T11:22:33.444',
'2023-05-26T11:22:33.444',
256,
'01',
'DS2',
'comment',
custom_worktype_id,
optionItemList,
false,
);
expect(result.jobNumber).toEqual('00000001');
// 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`],
{
authorId: 'AUTHOR_ID',
filename: 'file',
priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444',
},
);
// 作成したタスクを取得
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
// タスクのチェックアウト権限を取得
const resultCheckoutPermission = await getCheckoutPermissions(
source,
resultTask?.id ?? 0,
);
// タスクのテンプレートファイルIDを確認
expect(resultTask?.template_file_id).toEqual(templateFileId);
// タスクのチェックアウト権限が想定通りワークフローで設定されているのユーザーIDで作成されているか確認
expect(resultCheckoutPermission.length).toEqual(1);
expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId);
});
it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないものが入っていても自動ルーティングを行うことができるAPI実行者のAuthorIDとworkType', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー // 音声ファイルの録音者のユーザー
@ -705,7 +582,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
myExternalId, // API実行者のユーザーIDを設定 myExternalId, // API実行者のユーザーIDを設定
'http://blob/url/file.zip', 'http://blob/url/file.zip',
'XXXXXXXXXX', // 音声ファイルの情報には、録音者のAuthorIDが入る 'XXXXXX', // 存在しないAuthorIDを指定
'file.zip', 'file.zip',
'11:22:33', '11:22:33',
'2023-05-26T11:22:33.444', '2023-05-26T11:22:33.444',
@ -720,17 +597,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
false, false,
); );
expect(result.jobNumber).toEqual('00000001'); expect(result.jobNumber).toEqual('00000001');
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が呼ばれていないことを確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).not.toBeCalled();
makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`],
{
authorId: 'XXXXXXXXXX',
filename: 'file',
priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444',
},
);
// 作成したタスクを取得 // 作成したタスクを取得
const resultTask = await getTaskFromJobNumber(source, result.jobNumber); const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
// タスクのチェックアウト権限を取得 // タスクのチェックアウト権限を取得
@ -739,13 +607,12 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
resultTask?.id ?? 0, resultTask?.id ?? 0,
); );
// タスクのテンプレートファイルIDを確認 // タスクのテンプレートファイルIDを確認
expect(resultTask?.template_file_id).toEqual(templateFileId); expect(resultTask?.template_file_id).toBeNull();
// タスクのチェックアウト権限が想定通りワークフローで設定されているのユーザーIDで作成されているか確認 // 存在しないAuthorIDを指定してタスクを作成したためルーティングが行われず、タスクのチェックアウト権限は誰にも付与されない
expect(resultCheckoutPermission.length).toEqual(1); expect(resultCheckoutPermission.length).toEqual(0);
expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId);
}); });
it('ワークフローが見つからない場合、タスク作成時に自動ルーティングを行うことができない', async () => { it('ワークフローが見つからない場合、タスク作成時に自動ルーティングを行うことができない', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー // 音声ファイルの録音者のユーザー
@ -785,7 +652,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
'01', '01',
'DS2', 'DS2',
'comment', 'comment',
'worktypeId', '',
optionItemList, optionItemList,
false, false,
); );
@ -802,6 +669,98 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 自動ルーティングが行われていないことを確認 // 自動ルーティングが行われていないことを確認
expect(resultCheckoutPermission.length).toEqual(0); expect(resultCheckoutPermission.length).toEqual(0);
}); });
it('WorkTypeIDの指定がないワークフローで、タスク作成時に自動ルーティングを行うことができる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
// 音声ファイルの録音者のユーザー
const {
external_id: authorExternalId,
author_id: authorAuthorId,
id: authorUserId,
} = await makeTestUser(source, {
account_id: accountId,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const { id: typistUserId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'typist-user-external-id',
role: 'typist',
author_id: undefined,
});
// ワークフローを作成
const { id: workflowId } = await createWorkflow(
source,
accountId,
authorUserId,
undefined,
);
// ワークフロータイピストを作成
await createWorkflowTypist(source, workflowId, typistUserId);
// 初期値のジョブナンバーでjob_numberテーブルを作成
await createJobNumber(source, accountId, '00000000');
const blobParam = makeBlobstorageServiceMockValue();
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
const notificationHubService = module.get<NotificationhubService>(
NotificationhubService,
);
const result = await service.uploadFinished(
makeContext('trackingId', 'requestId'),
authorExternalId, // API実行者のユーザーIDを設定
'http://blob/url/file.zip',
authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る
'file.zip',
'11:22:33',
'2023-05-26T11:22:33.444',
'2023-05-26T11:22:33.444',
'2023-05-26T11:22:33.444',
256,
'01',
'DS2',
'comment',
'',
optionItemList,
false,
);
expect(result.jobNumber).toEqual('00000001');
// 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'),
`user_${typistUserId}`,
{
authorId: 'AUTHOR_ID',
filename: 'file',
id: '2',
priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444',
},
);
// タスクを取得
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
// タスクのチェックアウト権限を取得
const resultCheckoutPermission = await getCheckoutPermissions(
source,
resultTask?.id ?? 0,
);
// タスクがあることを確認
expect(resultTask).not.toBeNull();
// 自動ルーティングが行われていることを確認
expect(resultCheckoutPermission.length).toEqual(1);
expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId);
});
it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => { it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
@ -1452,10 +1411,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -1570,10 +1530,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -1698,10 +1659,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -1826,10 +1788,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -1944,10 +1907,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -2094,10 +2058,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -2243,10 +2208,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },
@ -2393,10 +2359,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
// 通知処理が想定通りの引数で呼ばれているか確認 // 通知処理が想定通りの引数で呼ばれているか確認
expect(notificationHubService.notify).toHaveBeenCalledWith( expect(notificationHubService.notify).toHaveBeenCalledWith(
makeContext('trackingId', 'requestId'), makeContext('trackingId', 'requestId'),
[`user_${typistUserId}`], `user_${typistUserId}`,
{ {
authorId: 'AUTHOR_ID', authorId: 'AUTHOR_ID',
filename: 'file', filename: 'file',
id: '2',
priority: 'High', priority: 'High',
uploadedAt: '2023-05-26T11:22:33.444', uploadedAt: '2023-05-26T11:22:33.444',
}, },

View File

@ -71,7 +71,8 @@ export class FilesService {
/** /**
* Uploads finished * Uploads finished
* @param url Blob Storage() * @param url Blob Storage()
* @param authorId ()AuthorID * @param userId ()UserID
* @param authorId AuthorのAuthorID
* @param fileName * @param fileName
* @param duration * @param duration
* @param createdDate ()yyyy-mm-ddThh:mm:ss.sss' * @param createdDate ()yyyy-mm-ddThh:mm:ss.sss'
@ -248,7 +249,6 @@ export class FilesService {
context, context,
task.audio_file_id, task.audio_file_id,
user.account_id, user.account_id,
user.author_id ?? undefined,
); );
const groupMembers = const groupMembers =
@ -273,12 +273,17 @@ export class FilesService {
this.logger.log(`[${context.getTrackingId()}] tags: ${tags}`); this.logger.log(`[${context.getTrackingId()}] tags: ${tags}`);
// タグ対象に通知送信 // タグ対象に通知送信
await this.notificationhubService.notify(context, tags, { await Promise.all(
authorId: authorId, tags.map((tag) => {
filename: fileName.replace('.zip', ''), return this.notificationhubService.notify(context, tag, {
priority: priority === '00' ? 'Normal' : 'High', id: tag.split('user_')[1],
uploadedAt: uploadedDate, authorId: authorId,
}); filename: fileName.replace('.zip', ''),
priority: priority === '00' ? 'Normal' : 'High',
uploadedAt: uploadedDate,
});
}),
);
// 追加したタスクのJOBナンバーを返却 // 追加したタスクのJOBナンバーを返却
return { jobNumber: task.job_number }; return { jobNumber: task.job_number };

View File

@ -27,6 +27,8 @@ import {
GetAllocatableLicensesResponse, GetAllocatableLicensesResponse,
CancelOrderRequest, CancelOrderRequest,
CancelOrderResponse, CancelOrderResponse,
IssueTrialLicenseResponse,
IssueTrialLicenseRequest,
} from './types/types'; } from './types/types';
import { Request } from 'express'; import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../common/http/helper'; import { retrieveAuthorizationToken } from '../../common/http/helper';
@ -347,6 +349,90 @@ export class LicensesController {
return allocatableLicenses; 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({ @ApiResponse({
status: HttpStatus.OK, status: HttpStatus.OK,
type: CancelOrderResponse, type: CancelOrderResponse,

View File

@ -1,4 +1,7 @@
import { NewAllocatedLicenseExpirationDate } from './types/types'; import {
NewAllocatedLicenseExpirationDate,
NewTrialLicenseExpirationDate,
} from './types/types';
import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { HttpException, HttpStatus } from '@nestjs/common'; import { HttpException, HttpStatus } from '@nestjs/common';
import { LicensesService } from './licenses.service'; import { LicensesService } from './licenses.service';
@ -15,12 +18,14 @@ import {
selectLicenseAllocationHistory, selectLicenseAllocationHistory,
createOrder, createOrder,
selectOrderLicense, selectOrderLicense,
selectIssuedLicensesAndLicenseOrders,
} from './test/utility'; } from './test/utility';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { Context, makeContext } from '../../common/log'; import { Context, makeContext } from '../../common/log';
import { import {
ADB2C_SIGN_IN_TYPE, ADB2C_SIGN_IN_TYPE,
LICENSE_ALLOCATED_STATUS, LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE, LICENSE_TYPE,
} from '../../constants'; } from '../../constants';
import { import {
@ -36,6 +41,7 @@ import {
} from '../../common/test/overrides'; } from '../../common/test/overrides';
import { truncateAllTable } from '../../common/test/init'; import { truncateAllTable } from '../../common/test/init';
import { TestLogger } from '../../common/test/logger'; import { TestLogger } from '../../common/test/logger';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
describe('ライセンス注文', () => { describe('ライセンス注文', () => {
let source: DataSource | null = null; let source: DataSource | null = null;
@ -103,6 +109,8 @@ describe('ライセンス注文', () => {
expect(dbSelectResult.orderLicense?.from_account_id).toEqual(accountId); expect(dbSelectResult.orderLicense?.from_account_id).toEqual(accountId);
expect(dbSelectResult.orderLicense?.to_account_id).toEqual(parentAccountId); expect(dbSelectResult.orderLicense?.to_account_id).toEqual(parentAccountId);
expect(dbSelectResult.orderLicense?.status).toEqual('Issue Requesting'); expect(dbSelectResult.orderLicense?.status).toEqual('Issue Requesting');
// ライセンス種別のデフォルト値が埋まっていること
expect(dbSelectResult.orderLicense?.type).toEqual(LICENSE_TYPE.NORMAL);
}); });
it('POナンバー重複時、エラーとなる', async () => { it('POナンバー重複時、エラーとなる', async () => {
@ -736,7 +744,7 @@ describe('ライセンス割り当て', () => {
); );
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
let _subject: string = ''; let _subject = '';
let _url: string | undefined = ''; let _url: string | undefined = '';
overrideAdB2cService(service, { overrideAdB2cService(service, {
getUser: async (context, externalId) => { getUser: async (context, externalId) => {
@ -2009,3 +2017,385 @@ describe('割り当て可能なライセンス取得', () => {
expect(response.allocatableLicenses[5].licenseId).toBe(1); 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,12 +14,19 @@ import { UserNotFoundError } from '../../repositories/users/errors/types';
import { import {
GetAllocatableLicensesResponse, GetAllocatableLicensesResponse,
IssueCardLicensesResponse, IssueCardLicensesResponse,
NewTrialLicenseExpirationDate,
} from './types/types'; } from './types/types';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { LICENSE_ISSUE_STATUS } from '../../constants'; import {
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
LICENSE_ISSUE_STATUS,
TIERS,
TRIAL_LICENSE_EXPIRATION_DAYS,
} from '../../constants';
import { User } from '../../repositories/users/entity/user.entity';
@Injectable() @Injectable()
export class LicensesService { export class LicensesService {
@ -452,6 +459,106 @@ export class LicensesService {
return; 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を指定して * IDを指定して
* @param context * @param context

View File

@ -1,4 +1,4 @@
import { DataSource } from 'typeorm'; import { DataSource, IsNull } from 'typeorm';
import { import {
License, License,
CardLicense, CardLicense,
@ -214,3 +214,45 @@ export const getLicenseAllocationHistoryArchive = async (
): Promise<LicenseAllocationHistoryArchive[]> => { ): Promise<LicenseAllocationHistoryArchive[]> => {
return await dataSource.getRepository(LicenseAllocationHistoryArchive).find(); 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,6 +59,16 @@ export class GetAllocatableLicensesResponse {
allocatableLicenses: AllocatableLicenseInfo[]; allocatableLicenses: AllocatableLicenseInfo[];
} }
export class IssueTrialLicenseRequest {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
issuedAccount: number;
}
export class IssueTrialLicenseResponse {}
export class CancelOrderRequest { export class CancelOrderRequest {
@ApiProperty() @ApiProperty()
@Matches(/^[A-Z0-9]+$/) @Matches(/^[A-Z0-9]+$/)

View File

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

View File

@ -132,6 +132,12 @@ export class TasksController {
const direction = isSortDirection(body.direction ?? '') const direction = isSortDirection(body.direction ?? '')
? (body.direction as SortDirection) ? (body.direction as SortDirection)
: undefined; : 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( const { tasks, total } = await this.taskService.getTasks(
context, context,
@ -143,6 +149,8 @@ export class TasksController {
status?.split(','), status?.split(','),
paramName, paramName,
direction, direction,
filterConditionAuthorId,
filterConditionFileName,
); );
return { tasks, total, limit, offset }; return { tasks, total, limit, offset };
} }
@ -831,4 +839,91 @@ export class TasksController {
await this.taskService.deleteTask(context, userId, audioFileId); await this.taskService.deleteTask(context, userId, audioFileId);
return {}; return {};
} }
@Post(':audioFileId/reopen')
@ApiResponse({
status: HttpStatus.OK,
type: ChangeStatusResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: '指定したIDの音声ファイルが存在しない場合',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'reopen',
description:
'完了した文字起こしタスクを再開しますステータスをPendingにします',
})
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN, USER_ROLES.TYPIST],
}),
)
@ApiBearerAuth()
async reopen(
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
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, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
await this.taskService.reopen(context, audioFileId, userId, roles);
return {};
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,8 @@ export class TasksService {
status?: string[], status?: string[],
paramName?: TaskListSortableAttribute, paramName?: TaskListSortableAttribute,
direction?: SortDirection, direction?: SortDirection,
filterConditionAuthorId?: string | null,
filterConditionFileName?: string | null,
): Promise<{ tasks: Task[]; total: number }> { ): Promise<{ tasks: Task[]; total: number }> {
this.logger.log( this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` + `[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` +
@ -84,7 +86,10 @@ export class TasksService {
`limit: ${limit}, ` + `limit: ${limit}, ` +
`status: ${status}, ` + `status: ${status}, ` +
`paramName: ${paramName}, ` + `paramName: ${paramName}, ` +
`direction: ${direction} };`, `direction: ${direction}, ` +
`filterConditionAuthorId: ${filterConditionAuthorId},` +
`filterConditionFileName: ${filterConditionFileName}
};`,
); );
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
@ -106,6 +111,8 @@ export class TasksService {
paramName ?? defaultParamName, paramName ?? defaultParamName,
direction ?? defaultDirection, direction ?? defaultDirection,
status ?? defaultStatus, status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
); );
// B2Cからユーザー名を取得する // B2Cからユーザー名を取得する
@ -134,6 +141,8 @@ export class TasksService {
paramName ?? defaultParamName, paramName ?? defaultParamName,
direction ?? defaultDirection, direction ?? defaultDirection,
status ?? defaultStatus, status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
); );
// B2Cからユーザー名を取得する // B2Cからユーザー名を取得する
@ -156,6 +165,8 @@ export class TasksService {
paramName ?? defaultParamName, paramName ?? defaultParamName,
direction ?? defaultDirection, direction ?? defaultDirection,
status ?? defaultStatus, status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
); );
// B2Cからユーザー名を取得する // B2Cからユーザー名を取得する
const b2cUsers = await this.getB2cUsers( const b2cUsers = await this.getB2cUsers(
@ -474,11 +485,7 @@ export class TasksService {
if (!typist) { if (!typist) {
throw new Error(`typist not found. id=${externalId}`); throw new Error(`typist not found. id=${externalId}`);
} }
const { displayName: typistName, emailAddress: typistEmail } = const { displayName: typistName } = getUserNameAndMailAddress(typist);
getUserNameAndMailAddress(typist);
if (!typistEmail) {
throw new Error(`typist email not found. id=${externalId}`);
}
const primaryAdmin = usersInfo.find( const primaryAdmin = usersInfo.find(
(x) => x.id === primaryAdminExternalId, (x) => x.id === primaryAdminExternalId,
@ -495,7 +502,6 @@ export class TasksService {
await this.sendgridService.sendMailWithU117( await this.sendgridService.sendMailWithU117(
context, context,
authorNotification ? authorEmail : null, authorNotification ? authorEmail : null,
typistEmail,
authorName, authorName,
task.file.file_name.replace('.zip', ''), task.file.file_name.replace('.zip', ''),
typistName, typistName,
@ -635,7 +641,6 @@ export class TasksService {
context, context,
audioFileId, audioFileId,
user.account_id, user.account_id,
user.author_id ?? undefined,
); );
// 通知を送信する // 通知を送信する
@ -718,6 +723,80 @@ export class TasksService {
} }
} }
/**
* (Pendingに変更する)
* @param audioFileId
* @param externalId
* @param role
* @returns reopen
*/
async reopen(
context: Context,
audioFileId: number,
externalId: string,
role: Roles[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.reopen.name
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
);
let user: User;
try {
// ユーザー取得
user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.reopen.name}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// roleにAdminが含まれていれば、文字起こし担当でなくても再開できるため、ユーザーIDは指定しない
await this.taskRepository.reopen(
context,
audioFileId,
[TASK_STATUS.FINISHED],
user.account_id,
role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.NOT_FOUND,
);
case StatusNotMatchError:
case TypistUserNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
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.reopen.name}`);
}
}
/** /**
* backupする * backupする
* @param context * @param context
@ -1043,12 +1122,18 @@ export class TasksService {
} }
// タグ対象に通知送信 // タグ対象に通知送信
await this.notificationhubService.notify(context, tags, { await Promise.all(
authorId: file.author_id, tags.map((tag) => {
filename: file.file_name.replace('.zip', ''), return this.notificationhubService.notify(context, tag, {
priority: file.priority === '00' ? 'Normal' : 'High', id: tag.split('user_')[1],
uploadedAt: file.uploaded_at.toISOString(), authorId: file.author_id,
}); filename: file.file_name.replace('.zip', ''),
priority: file.priority === '00' ? 'Normal' : 'High',
uploadedAt: file.uploaded_at.toISOString(),
});
}),
);
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendNotify.name}`, `[OUT] [${context.getTrackingId()}] ${this.sendNotify.name}`,
); );

View File

@ -27,6 +27,7 @@ import { NotificationhubModule } from '../../../gateways/notificationhub/notific
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module'; import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module'; import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.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 { AuthService } from '../../../features/auth/auth.service';
import { AccountsService } from '../../../features/accounts/accounts.service'; import { AccountsService } from '../../../features/accounts/accounts.service';
import { UsersService } from '../../../features/users/users.service'; import { UsersService } from '../../../features/users/users.service';
@ -74,6 +75,7 @@ export const makeTaskTestingModuleWithNotificaiton = async (
BlobstorageModule, BlobstorageModule,
AuthGuardsModule, AuthGuardsModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
], ],
providers: [ providers: [
AuthService, AuthService,
@ -110,8 +112,9 @@ export const createTask = async (
priority: string, priority: string,
jobNumber: string, jobNumber: string,
status: string, status: string,
typist_user_id?: number | undefined, typist_user_id?: number,
is_job_number_enabled?: boolean | undefined, is_job_number_enabled?: boolean,
file_name?: string,
): Promise<{ taskId: number; audioFileId: number }> => { ): Promise<{ taskId: number; audioFileId: number }> => {
const { identifiers: audioFileIdentifiers } = await datasource const { identifiers: audioFileIdentifiers } = await datasource
.getRepository(AudioFile) .getRepository(AudioFile)
@ -119,8 +122,8 @@ export const createTask = async (
account_id: account_id, account_id: account_id,
owner_user_id: owner_user_id, owner_user_id: owner_user_id,
url: '', url: '',
file_name: 'x.zip', file_name: file_name ?? 'x.zip',
raw_file_name: 'y.zip', raw_file_name: file_name ?? 'y.zip',
author_id: author_id, author_id: author_id,
work_type_id: work_type_id, work_type_id: work_type_id,
started_at: new Date(), started_at: new Date(),

View File

@ -6,6 +6,7 @@ import {
IsIn, IsIn,
IsInt, IsInt,
IsOptional, IsOptional,
IsString,
Min, Min,
ValidateNested, ValidateNested,
} from 'class-validator'; } from 'class-validator';
@ -65,6 +66,22 @@ export class TasksRequest {
}) })
@IsOptional() @IsOptional()
paramName?: string; paramName?: string;
@ApiProperty({
required: false,
description: `タスクの検索キーワード:AuthorID`,
})
@IsString()
@IsOptional()
authorId?: string;
@ApiProperty({
required: false,
description: `タスクの検索キーワード:fileName`,
})
@IsString()
@IsOptional()
fileName?: string;
} }
// TODO: RequestでもResponseでも使われているので、Requestに使用される箇所のみバリデータでチェックが行われる状態になっている // TODO: RequestでもResponseでも使われているので、Requestに使用される箇所のみバリデータでチェックが行われる状態になっている

View File

@ -12,6 +12,8 @@ import { LicensesRepositoryService } from '../../../repositories/licenses/licens
import { UsersService } from '../users.service'; import { UsersService } from '../users.service';
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity'; import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service'; 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 { import {
SortDirection, SortDirection,
TaskListSortableAttribute, TaskListSortableAttribute,
@ -26,6 +28,11 @@ export type SortCriteriaRepositoryMockValue = {
getSortCriteria: SortCriteria | Error; getSortCriteria: SortCriteria | Error;
}; };
export type TaskFiltersRepositoryMockValue = {
updateTaskFilter: TaskFilters | Error;
getTaskFilter: TaskFilters | Error;
};
export type UsersRepositoryMockValue = { export type UsersRepositoryMockValue = {
updateUserVerified: undefined | Error; updateUserVerified: undefined | Error;
findUserById: User | Error; findUserById: User | Error;
@ -63,6 +70,7 @@ export const makeUsersServiceMock = async (
sendGridMockValue: SendGridMockValue, sendGridMockValue: SendGridMockValue,
configMockValue: ConfigMockValue, configMockValue: ConfigMockValue,
sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue: TaskFiltersRepositoryMockValue,
): Promise<UsersService> => { ): Promise<UsersService> => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [UsersService], providers: [UsersService],
@ -90,6 +98,8 @@ export const makeUsersServiceMock = async (
return makeSortCriteriaRepositoryMock( return makeSortCriteriaRepositoryMock(
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
); );
case TaskFiltersRepositoryService:
return makeTaskFiltersRepositoryMock(taskFiltersRepositoryMockValue);
case BlobstorageService: case BlobstorageService:
return {}; return {};
} }
@ -128,6 +138,29 @@ export const makeSortCriteriaRepositoryMock = (
}; };
}; };
export const makeTaskFiltersRepositoryMock = (
value: TaskFiltersRepositoryMockValue,
) => {
const { updateTaskFilter, getTaskFilter } = value;
return {
updateTaskFilter:
updateTaskFilter instanceof Error
? jest
.fn<Promise<void>, [number, string, string]>()
.mockRejectedValue(updateTaskFilter)
: jest
.fn<Promise<TaskFilters>, [number, string, string]>()
.mockResolvedValue(updateTaskFilter),
getTaskFilter:
getTaskFilter instanceof Error
? jest.fn<Promise<void>, [number]>().mockRejectedValue(getTaskFilter)
: jest
.fn<Promise<TaskFilters>, [number]>()
.mockResolvedValue(getTaskFilter),
};
};
export const makeSendGridServiceMock = (value: SendGridMockValue) => { export const makeSendGridServiceMock = (value: SendGridMockValue) => {
const { sendMail } = value; const { sendMail } = value;
return { return {
@ -291,6 +324,21 @@ export const makeDefaultSortCriteriaRepositoryMockValue =
}; };
}; };
export const makeDefaultTaskFiltersRepositoryMockValue =
(): TaskFiltersRepositoryMockValue => {
const taskFilter = new TaskFilters();
{
taskFilter.id = 1;
taskFilter.author_id = null;
taskFilter.file_name = null;
taskFilter.user_id = 1;
}
return {
updateTaskFilter: taskFilter,
getTaskFilter: taskFilter,
};
};
export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
return { return {
getMetaData: { getMetaData: {

View File

@ -35,6 +35,15 @@ export class ConfirmRequest {
export class ConfirmResponse {} export class ConfirmResponse {}
export class ConfirmForceRequest {
@ApiProperty()
@IsInt()
userId: number;
}
export class ConfirmForceResponse {}
export class User { export class User {
@ApiProperty() @ApiProperty()
id: number; id: number;
@ -84,6 +93,17 @@ export class User {
licenseStatus: string; licenseStatus: string;
} }
export class GetUsersRequest {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
userName?: string;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
email?: string;
}
export class GetUsersResponse { export class GetUsersResponse {
@ApiProperty({ type: [User] }) @ApiProperty({ type: [User] })
users: User[]; users: User[];
@ -223,6 +243,30 @@ export class GetSortCriteriaResponse {
paramName: string; 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 { export class PostUpdateUserRequest {
@ApiProperty() @ApiProperty()
@Type(() => Number) @Type(() => Number)

View File

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

View File

@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.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 { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
import { UsersController } from './users.controller'; import { UsersController } from './users.controller';
@ -17,6 +18,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module
UsersRepositoryModule, UsersRepositoryModule,
LicensesRepositoryModule, LicensesRepositoryModule,
SortCriteriaRepositoryModule, SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
AdB2cModule, AdB2cModule,
SendGridModule, SendGridModule,
ConfigModule, ConfigModule,

View File

@ -5,6 +5,7 @@ import {
makeDefaultConfigValue, makeDefaultConfigValue,
makeDefaultSendGridlValue, makeDefaultSendGridlValue,
makeDefaultSortCriteriaRepositoryMockValue, makeDefaultSortCriteriaRepositoryMockValue,
makeDefaultTaskFiltersRepositoryMockValue,
makeDefaultUsersRepositoryMockValue, makeDefaultUsersRepositoryMockValue,
makeUsersServiceMock, makeUsersServiceMock,
} from './test/users.service.mock'; } from './test/users.service.mock';
@ -60,6 +61,7 @@ import { createCheckoutPermissions } from '../tasks/test/utility';
import { MultipleImportErrors } from './types/types'; import { MultipleImportErrors } from './types/types';
import { TestLogger } from '../../common/test/logger'; import { TestLogger } from '../../common/test/logger';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { CUSTOMER_NAME } from '../../templates/constants';
describe('UsersService.confirmUser', () => { describe('UsersService.confirmUser', () => {
let source: DataSource | null = null; let source: DataSource | null = null;
@ -123,7 +125,7 @@ describe('UsersService.confirmUser', () => {
}); });
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
let _subject: string = ''; let _subject = '';
let _url: string | undefined = ''; let _url: string | undefined = '';
overrideSendgridService(service, { overrideSendgridService(service, {
sendMail: async ( sendMail: async (
@ -320,7 +322,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
}; };
}, },
}); });
let _subject: string = ''; let _subject = '';
overrideSendgridService(service, { overrideSendgridService(service, {
sendMail: async ( sendMail: async (
context: Context, context: Context,
@ -892,7 +894,7 @@ describe('UsersService.createUser', () => {
}; };
}, },
}); });
let _subject: string = ''; let _subject = '';
let _url: string | undefined = ''; let _url: string | undefined = '';
overrideSendgridService(service, { overrideSendgridService(service, {
sendMail: async ( sendMail: async (
@ -2049,6 +2051,331 @@ describe('UsersService.getUsers', () => {
); );
}); });
it('ユーザーを取得できること(名前入力メール未入力検索)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(
source,
{
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
},
);
const { id: user2 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id2',
role: 'author',
author_id: 'AUTHOR_ID2',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const { id: user3 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id3',
role: 'author',
author_id: 'AUTHOR_ID3',
auto_renew: false,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, 'test1', undefined);
expect(result.length).toBe(1);
expect(result[0].name).toBe('test1');
});
it('ユーザーを取得できること(名前未入力メール入力検索)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(
source,
{
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
},
);
const { id: user2 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id2',
role: 'author',
author_id: 'AUTHOR_ID2',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const { id: user3 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id3',
role: 'author',
author_id: 'AUTHOR_ID3',
auto_renew: false,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, undefined, 'test2@mail.com');
expect(result.length).toBe(1);
expect(result[0].email).toBe('test2@mail.com');
});
it('ユーザーを取得できること(名前/メール入力検索)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(
source,
{
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
},
);
const { id: user2 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id2',
role: 'author',
author_id: 'AUTHOR_ID2',
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const { id: user3 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id3',
role: 'author',
author_id: 'AUTHOR_ID3',
auto_renew: false,
encryption: false,
encryption_password: undefined,
prompt: false,
});
const expectedUser = {
id: user3,
name: 'test3',
role: 'author',
authorId: 'AUTHOR_ID3',
typistGroupName: [],
email: 'test3@mail.com',
emailVerified: true,
autoRenew: false,
notification: true,
encryption: false,
prompt: false,
expiration: undefined,
remaining: undefined,
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
}
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, '3', 'test3@mail');
expect(result.length).toBe(1);
expect(result[0]).toEqual(expectedUser);
});
it('ユーザーを取得できること名前入力メール未入力検索で0件', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, 'nonexistent', undefined);
expect(result.length).toBe(0);
});
it('ユーザーを取得できること名前未入力メール入力検索で0件', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, undefined, 'wrongemail@example.com');
expect(result.length).toBe(0);
});
it('ユーザーを取得できること(名前/メール入力で0件', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, 'test1', 'wrongemail@example.com');
expect(result.length).toBe(0);
});
it('ユーザーを取得できること(名前/メール入力で0件', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1, 'wronguser', 'test1@mail.com');
expect(result.length).toBe(0);
});
it('ユーザーを取得できること(名前メール未入力)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id1',
role: 'author',
author_id: 'AUTHOR_ID1',
});
const { id: user2 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id2',
role: 'author',
author_id: 'AUTHOR_ID2',
});
const { id: user3 } = await makeTestUser(source, {
account_id: accountId,
external_id: 'external_id3',
role: 'author',
author_id: 'AUTHOR_ID3',
});
const expectedUsers = [
{
id: user1,
name: 'test1',
role: 'author',
authorId: 'AUTHOR_ID1',
typistGroupName: [],
email: 'test1@mail.com',
emailVerified: true,
autoRenew: true,
notification: true,
encryption: true,
prompt: true,
expiration: undefined,
remaining: undefined,
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
{
id: user2,
name: 'test2',
role: 'author',
authorId: 'AUTHOR_ID2',
typistGroupName: [],
email: 'test2@mail.com',
emailVerified: true,
autoRenew: true,
notification: true,
encryption: true,
prompt: true,
expiration: undefined,
remaining: undefined,
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
{
id: user3,
name: 'test3',
role: 'author',
authorId: 'AUTHOR_ID3',
typistGroupName: [],
email: 'test3@mail.com',
emailVerified: true,
autoRenew: true,
notification: true,
encryption: true,
prompt: true,
expiration: undefined,
remaining: undefined,
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
]
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
const result = await service.getUsers(context, external_id1);
expect(result.length).toBe(3);
expect(result).toEqual(expectedUsers)
});
it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => { it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => {
const adb2cParam = makeDefaultAdB2cMockValue(); const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail(); if (!source) fail();
@ -2112,6 +2439,8 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock( const service = await makeUsersServiceMock(
usersRepositoryMockValue, usersRepositoryMockValue,
licensesRepositoryMockValue, licensesRepositoryMockValue,
@ -2119,6 +2448,7 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2140,6 +2470,8 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('user not found'); usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
@ -2150,6 +2482,7 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2171,6 +2504,8 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.updateSortCriteria = new Error( sortCriteriaRepositoryMockValue.updateSortCriteria = new Error(
'sort criteria not found', 'sort criteria not found',
); );
@ -2182,6 +2517,7 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2205,6 +2541,8 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock( const service = await makeUsersServiceMock(
usersRepositoryMockValue, usersRepositoryMockValue,
licensesRepositoryMockValue, licensesRepositoryMockValue,
@ -2212,6 +2550,7 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2229,6 +2568,8 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = new Error( sortCriteriaRepositoryMockValue.getSortCriteria = new Error(
'sort criteria not found', 'sort criteria not found',
@ -2241,6 +2582,7 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2262,6 +2604,8 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue(); const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue = const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue(); makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = { sortCriteriaRepositoryMockValue.getSortCriteria = {
id: 1, id: 1,
direction: 'AAA', direction: 'AAA',
@ -2276,6 +2620,7 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue, sendgridMockValue,
configMockValue, configMockValue,
sortCriteriaRepositoryMockValue, sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
); );
const context = makeContext(`uuidv4`, 'requestId'); const context = makeContext(`uuidv4`, 'requestId');
@ -2290,6 +2635,182 @@ describe('UsersService.getSortCriteria', () => {
}); });
}); });
describe('UsersService.updateTaskFilter', () => {
it('タスク検索条件を変更できる', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
expect(
await service.updateTaskFilter(
context,
'AUTHOR_ID',
'FILE_NAME',
'external_id',
),
).toEqual(undefined);
});
it('ユーザー情報が存在せず、タスク検索条件を変更できない', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
await expect(
service.updateTaskFilter(
context,
'AUTHOR_ID',
'FILE_NAME',
'external_id',
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('タスク検索条件が存在せず、タスク検索条件を変更できない', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
taskFiltersRepositoryMockValue.updateTaskFilter = new Error(
'task filters not found',
);
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
await expect(
service.updateTaskFilter(
context,
'AUTHOR_ID',
'FILE_NAME',
'external_id',
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});
describe('UsersService.getTaskFilter', () => {
it('タスク検索条件を取得できる', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
console.log(await service.getTaskFilter(context, 'external_id'));
expect(await service.getTaskFilter(context, 'external_id')).toEqual({
authorId: undefined,
fileName: undefined,
});
});
it('タスク検索条件が存在せず、タスク検索条件を取得できない', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
taskFiltersRepositoryMockValue.getTaskFilter = new Error(
'task filters not found',
);
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
await expect(service.getTaskFilter(context, 'external_id')).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});
describe('UsersService.updateUser', () => { describe('UsersService.updateUser', () => {
let source: DataSource | null = null; let source: DataSource | null = null;
beforeAll(async () => { beforeAll(async () => {
@ -3417,7 +3938,7 @@ describe('UsersService.deleteUser', () => {
const userArchive = await getUserArchive(source); const userArchive = await getUserArchive(source);
expect(userArchive[0].external_id).toBe(external_id); expect(userArchive[0].external_id).toBe(external_id);
} }
},600000); });
it('存在しないユーザは削除できない', async () => { it('存在しないユーザは削除できない', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
@ -4568,7 +5089,7 @@ describe('UsersService.deleteUser', () => {
fail(); fail();
} }
} }
},600000); });
it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがBackup)', async () => { it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがBackup)', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
@ -5144,3 +5665,295 @@ describe('UsersService.multipleImportsComplate', () => {
); );
}); });
}); });
describe('UsersService.confirmUserForce', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
logger: new TestLogger('none'),
logging: true,
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {
await truncateAllTable(source);
}
});
afterAll(async () => {
await source?.destroy();
source = null;
});
it('第五階層の管理者がメール認証済みではないユーザーを強制認証できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { account, admin } = await makeTestAccount(source, {
tier: 5,
});
const { id: user1, external_id } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_1',
email_verified: false,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
overrideAdB2cService(service, {
getUsers: async () => {
return [
{
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'admin@example.com',
},
],
},
{
id: external_id,
displayName: 'user1',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
},
];
},
getUser: async () => {
return {
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
};
},
});
let mailSubject: string | undefined;
let mailText: string | undefined;
let mailTextUrl: string | undefined;
let mailHtml: string | undefined;
let mailHtmlUrl: string | undefined;
let _to: string[] | undefined;
let _cc: string[] | undefined;
overrideSendgridService(service, {
sendMail: jest.fn(
async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const mailTextUrls = text.match(urlPattern);
const mailHtmlUrls = html.match(urlPattern);
mailSubject = subject;
mailText = text;
mailTextUrl = mailTextUrls?.pop();
mailHtml = html;
mailHtmlUrl = mailHtmlUrls?.pop();
_to = to;
_cc = cc;
},
),
});
// 強制認証を実行
await service.confirmUserForce(context, user1);
// ユーザーのメール認証済みに変更されたことを確認
{
const user = await getUser(source, user1);
if (!user) fail();
expect(user.email_verified).toBe(true);
}
// メールの検証
expect(mailSubject).toBe('Forced Email Verification Notification [U-126]');
expect(mailText?.includes('admin')).toBe(true);
expect(mailHtml?.includes('admin')).toBe(true);
expect(_to).toEqual(['user1@example.com']);
// ユーザー取得をモック化しているため、値の比較ではなく有り無しで確認
expect(mailText?.includes(CUSTOMER_NAME)).toBe(false);
expect(mailTextUrl).toBe('http://localhost:8081/');
expect(_cc).not.toBeUndefined();
expect(mailHtml?.includes(CUSTOMER_NAME)).toBe(false);
expect(mailHtmlUrl).toBe('http://localhost:8081/');
});
it('存在しないユーザは強制認証できない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { account, admin } = await makeTestAccount(source, {
tier: 5,
});
const { external_id } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.AUTHOR,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
overrideAdB2cService(service, {
getUsers: async () => {
return [
{
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'admin@example.com',
},
],
},
{
id: external_id,
displayName: 'user1',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
},
];
},
getUser: async () => {
return {
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
};
},
});
overrideSendgridService(service, {});
try {
await service.confirmUserForce(context, 100);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
it('既に認証済みのユーザは強制認証できない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { account, admin } = await makeTestAccount(source, {
tier: 5,
});
const { external_id } = await makeTestUser(source, {
account_id: account.id,
role: USER_ROLES.AUTHOR,
// 認証済みユーザー
email_verified: true,
});
const service = module.get<UsersService>(UsersService);
const context = makeContext(`uuidv4`, 'requestId');
overrideAdB2cService(service, {
getUsers: async () => {
return [
{
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'admin@example.com',
},
],
},
{
id: external_id,
displayName: 'user1',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
},
];
},
getUser: async () => {
return {
id: admin.external_id,
displayName: 'admin',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'user1@example.com',
},
],
};
},
});
overrideSendgridService(service, {});
try {
await service.confirmUserForce(context, admin.id);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010202'));
} else {
fail();
}
}
});
});

View File

@ -17,6 +17,7 @@ import {
} from '../../gateways/adb2c/adb2c.service'; } from '../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service'; import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
import { TaskFiltersRepositoryService } from '../../repositories/task_filters/task_filter.repository.service';
import { import {
User as EntityUser, User as EntityUser,
newUser, newUser,
@ -26,6 +27,7 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses.
import { import {
MultipleImportUser, MultipleImportUser,
GetRelationsResponse, GetRelationsResponse,
GetTaskFiltersResponse,
MultipleImportErrors, MultipleImportErrors,
User, User,
} from './types/types'; } from './types/types';
@ -73,6 +75,7 @@ export class UsersService {
private readonly usersRepository: UsersRepositoryService, private readonly usersRepository: UsersRepositoryService,
private readonly licensesRepository: LicensesRepositoryService, private readonly licensesRepository: LicensesRepositoryService,
private readonly sortCriteriaRepository: SortCriteriaRepositoryService, private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
private readonly taskFiltersRepository: TaskFiltersRepositoryService,
private readonly adB2cService: AdB2cService, private readonly adB2cService: AdB2cService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly sendgridService: SendGridService, private readonly sendgridService: SendGridService,
@ -603,10 +606,18 @@ export class UsersService {
/** /**
* Get Users * Get Users
* @param accessToken * @param context
* @param externalId
* @param userInputUserName
* @param userInputEmail
* @returns users * @returns users
*/ */
async getUsers(context: Context, externalId: string): Promise<User[]> { async getUsers(
context: Context,
externalId: string,
userInputUserName?: string,
userInputEmail?: string,
): Promise<User[]> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`); this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
try { try {
@ -617,7 +628,7 @@ export class UsersService {
); );
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
const externalIds = dbUsers.map((x) => x.external_id); const externalIds = dbUsers.map((user) => user.external_id);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出 // DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
@ -703,7 +714,22 @@ export class UsersService {
}; };
}); });
return users; // 検索条件(ユーザ名とメールアドレス)が入力されていない場合は全ユーザーを返す
if (!userInputUserName && !userInputEmail) {
return users;
}
// 検索条件が入力されている場合、部分一致するユーザーだけを残す
const matchedUsers = users.filter(
(user) =>
(!userInputUserName ||
user.name
.toLowerCase()
.includes(userInputUserName.toLowerCase())) &&
(!userInputEmail ||
user.email.toLowerCase().includes(userInputEmail.toLowerCase())),
);
return matchedUsers;
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException( throw new HttpException(
@ -831,6 +857,116 @@ export class UsersService {
} }
} }
/**
* Updates task filters
* @param authorId
* @param fileName
* @param token
* @returns task filters
*/
async updateTaskFilter(
context: Context,
filterConditionAuthorId: string | null,
filterConditionFileName: string | null,
externalId: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateTaskFilter.name
} | params: { filterConditionAuthorId: ${filterConditionAuthorId}, filterConditionFileName: ${filterConditionFileName}, externalId: ${externalId} };`,
);
let user: EntityUser;
try {
// ユーザー情報を取得
user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// ユーザーの検索条件を更新
await this.taskFiltersRepository.updateTaskFilter(
user.id,
filterConditionAuthorId,
filterConditionFileName,
context,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateTaskFilter.name}`,
);
}
}
/**
* Gets task filters
* @param token
* @returns task filters
*/
async getTaskFilter(
context: Context,
externalId: string,
): Promise<GetTaskFiltersResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getTaskFilter.name
} | params: { externalId: ${externalId} };`,
);
let user: EntityUser;
try {
// ユーザー情報を取得
user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// ユーザーのタスク検索条件を取得
const taskFilters = await this.taskFiltersRepository.getTaskFilter(
user.id,
context,
);
const { author_id: authorId, file_name: fileName } = taskFilters;
const result = {
authorId: authorId ?? undefined,
fileName: fileName ?? undefined,
};
return result;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getTaskFilter.name}`,
);
}
}
/** /**
* *
* @param userId * @param userId
@ -1740,6 +1876,99 @@ export class UsersService {
} }
} }
/**
*
* @param userId Id
*/
async confirmUserForce(context: Context, userId: number): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
);
try {
// ユーザーをメール認証済みにする。
await this.usersRepository.updateUserVerified(context, userId);
// 通知先のユーザーを取得
const { external_id, account_id } =
await this.usersRepository.findUserById(context, userId);
const adb2cUser = await this.adB2cService.getUser(context, external_id);
const { displayName, emailAddress } =
getUserNameAndMailAddress(adb2cUser);
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
if (!emailAddress) {
throw new UserNotFoundError(
`emailAddress is null. externalId=${external_id}`,
);
}
// プライマリアカウント管理者を取得する
const { primary_admin_user_id } =
await this.accountsRepository.findAccountById(context, account_id);
if (primary_admin_user_id === null) {
throw new UserNotFoundError(
`primary_admin_user_id is null. account_id=${account_id}`,
);
}
const { external_id: primaryUserExtarnalId } =
await this.usersRepository.findUserById(context, primary_admin_user_id);
const primaryAdmimAdb2cUser = await this.adB2cService.getUser(
context,
primaryUserExtarnalId,
);
const {
emailAddress: primaryAdminMailAdress,
} = getUserNameAndMailAddress(primaryAdmimAdb2cUser);
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
if (!primaryAdminMailAdress) {
throw new UserNotFoundError(
`primary admin emailAddress is null. externalId=${primaryUserExtarnalId}`,
);
}
try {
// アカウント認証が完了した旨をメール送信する
await this.sendgridService.sendMailWithU126(
context,
emailAddress,
displayName,
primaryAdminMailAdress,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case EmailAlreadyVerifiedError:
throw new HttpException(
makeErrorResponse('E010202'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
);
}
}
/** /**
* IDを指定して * IDを指定して
* @param context * @param context

View File

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

View File

@ -9,7 +9,6 @@ import {
createAppleInstallation, createAppleInstallation,
createWindowsRawNotification, createWindowsRawNotification,
} from '@azure/notification-hubs'; } from '@azure/notification-hubs';
import { TAG_MAX_COUNT } from '../../constants';
import { PNS } from '../../constants'; import { PNS } from '../../constants';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { NotificationBody } from '../../common/notify/types/types'; import { NotificationBody } from '../../common/notify/types/types';
@ -83,65 +82,57 @@ export class NotificationhubService {
/** /**
* *
* @param context * @param context
* @param tags * @param tag
* @param bodyContent * @param bodyContent
* @returns notify * @returns notify
*/ */
async notify( async notify(
context: Context, context: Context,
tags: string[], tag: string,
bodyContent: NotificationBody, bodyContent: NotificationBody,
): Promise<void> { ): Promise<void> {
this.logger.log( this.logger.log(
`[IN] [${context.getTrackingId()}] ${ `[IN] [${context.getTrackingId()}] ${
this.notify.name this.notify.name
} | params: { tags: ${tags}, bodyContent: ${JSON.stringify( } | params: { tag: ${tag}, bodyContent: ${JSON.stringify(bodyContent)} }`,
bodyContent,
)} }`,
); );
try { try {
// OR条件によるtag指定は20個までなので分割して送信する const tagExpression = createTagExpression([tag]);
const chunkTags = splitArrayInChunks(tags, TAG_MAX_COUNT);
for (let index = 0; index < chunkTags.length; index++) { // Windows
const currentTags = chunkTags[index]; try {
const tagExpression = createTagExpression(currentTags); const body = {
wns: {
// Windows alert: '',
try { },
const body = { newDictation: bodyContent,
wns: { };
alert: '', const notification = createWindowsRawNotification({
}, body: JSON.stringify(body),
newDictation: bodyContent, });
}; const result = await this.client.sendNotification(notification, {
const notification = createWindowsRawNotification({ tagExpression,
body: JSON.stringify(body), });
}); this.logger.log(`[${context.getTrackingId()}] ${result}`);
const result = await this.client.sendNotification(notification, { } catch (e) {
tagExpression, this.logger.error(`[${context.getTrackingId()}] error=${e}`);
}); }
this.logger.log(`[${context.getTrackingId()}] ${result}`); // Apple
} catch (e) { try {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); const body = createAppleNotificationBody({
} aps: {
// Apple alert: '',
try { },
const body = createAppleNotificationBody({ newDictation: bodyContent,
aps: { });
alert: '', const notification = createAppleNotification({ body });
}, const result = await this.client.sendNotification(notification, {
newDictation: bodyContent, tagExpression,
}); });
const notification = createAppleNotification({ body }); this.logger.log(`[${context.getTrackingId()}] ${result}`);
const result = await this.client.sendNotification(notification, { } catch (e) {
tagExpression, this.logger.error(`[${context.getTrackingId()}] error=${e}`);
});
this.logger.log(`[${context.getTrackingId()}] ${result}`);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
}
} }
} catch (e) { } catch (e) {
throw e; throw e;
@ -150,11 +141,3 @@ export class NotificationhubService {
} }
} }
} }
const splitArrayInChunks = (arr: string[], size: number): string[][] => {
const result: string[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};

View File

@ -33,6 +33,8 @@ import {
NO_ERROR_MESSAGE_EN, NO_ERROR_MESSAGE_EN,
NO_ERROR_MESSAGE_DE, NO_ERROR_MESSAGE_DE,
NO_ERROR_MESSAGE_FR, NO_ERROR_MESSAGE_FR,
ISSUER_CUSTOMER_NAME,
EXPIRATION_DATE,
} from '../../templates/constants'; } from '../../templates/constants';
import { URL } from 'node:url'; import { URL } from 'node:url';
@ -100,6 +102,10 @@ export class SendGridService {
private readonly templateU123Text: string; private readonly templateU123Text: string;
private readonly templateU124Html: string; private readonly templateU124Html: string;
private readonly templateU124Text: 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) { constructor(private readonly configService: ConfigService) {
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN'); this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
@ -357,6 +363,22 @@ export class SendGridService {
path.resolve(__dirname, `../../templates/template_U_124.txt`), path.resolve(__dirname, `../../templates/template_U_124.txt`),
'utf-8', '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',
);
} }
} }
@ -379,11 +401,11 @@ export class SendGridService {
const subject = 'Account Registered Notification [U-101]'; const subject = 'Account Registered Notification [U-101]';
const url = new URL(this.appDomain).href; const url = new URL(this.appDomain).href;
const html = this.templateU101Html const html = this.templateU101Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
const text = this.templateU101Text const text = this.templateU101Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
await this.sendMail( await this.sendMail(
context, context,
@ -434,8 +456,8 @@ export class SendGridService {
const verifyUrl = `${url}?verify=${token}`; const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-102]'; const subject = 'User Registration Notification [U-102]';
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl); const html = this.templateU102Html.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
const text = this.templateU102Text.replaceAll(VERIFY_LINK, verifyUrl); const text = this.templateU102Text.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
await this.sendMail( await this.sendMail(
context, context,
@ -481,15 +503,15 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU105Html const html = this.templateU105Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
const text = this.templateU105Text const text = this.templateU105Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -536,15 +558,15 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU106Html const html = this.templateU106Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
const text = this.templateU106Text const text = this.templateU106Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -591,15 +613,15 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU107Html const html = this.templateU107Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
const text = this.templateU107Text const text = this.templateU107Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -648,28 +670,28 @@ export class SendGridService {
if (dealerAccountName === null) { if (dealerAccountName === null) {
html = this.templateU108NoParentHtml html = this.templateU108NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
text = this.templateU108NoParentText text = this.templateU108NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
} else { } else {
html = this.templateU108Html html = this.templateU108Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
text = this.templateU108Text text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
} }
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail]; const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
@ -719,15 +741,15 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU109Html const html = this.templateU109Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
const text = this.templateU109Text const text = this.templateU109Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, poNumber) .replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); .replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -769,14 +791,14 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU111Html const html = this.templateU111Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
const text = this.templateU111Text const text = this.templateU111Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -825,24 +847,24 @@ export class SendGridService {
if (dealerAccountName === null) { if (dealerAccountName === null) {
// メールの本文を作成する // メールの本文を作成する
html = this.templateU112NoParentHtml html = this.templateU112NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
text = this.templateU112NoParentText text = this.templateU112NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
} else { } else {
html = this.templateU112Html html = this.templateU112Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
text = this.templateU112Text text = this.templateU112Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
} }
// メールを送信する // メールを送信する
@ -884,11 +906,11 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU113Html const html = this.templateU113Html
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword); .replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
const text = this.templateU113Text const text = this.templateU113Text
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword); .replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -947,11 +969,11 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU114Html const html = this.templateU114Html
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(VERIFY_LINK, verifyUrl); .replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
const text = this.templateU114Text const text = this.templateU114Text
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(VERIFY_LINK, verifyUrl); .replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -994,11 +1016,11 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU115Html const html = this.templateU115Html
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
const text = this.templateU115Text const text = this.templateU115Text
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
// 管理者ユーザーの情報を変更した場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない // 管理者ユーザーの情報を変更した場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない
const ccAdminMails = adminMails.filter((x) => x !== userMail); const ccAdminMails = adminMails.filter((x) => x !== userMail);
@ -1044,11 +1066,11 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU116Html const html = this.templateU116Html
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
const text = this.templateU116Text const text = this.templateU116Text
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -1081,7 +1103,6 @@ export class SendGridService {
async sendMailWithU117( async sendMailWithU117(
context: Context, context: Context,
authorEmail: string | null, authorEmail: string | null,
typistEmail: string,
authorName: string, authorName: string,
fileName: string, fileName: string,
typistName: string, typistName: string,
@ -1095,26 +1116,24 @@ export class SendGridService {
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU117Html const html = this.templateU117Html
.replaceAll(AUTHOR_NAME, authorName) .replaceAll(AUTHOR_NAME, escapeDollar(authorName))
.replaceAll(FILE_NAME, fileName) .replaceAll(FILE_NAME, escapeDollar(fileName))
.replaceAll(TYPIST_NAME, typistName) .replaceAll(TYPIST_NAME, escapeDollar(typistName))
.replaceAll(PRIMARY_ADMIN_NAME, adminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(adminName));
const text = this.templateU117Text const text = this.templateU117Text
.replaceAll(AUTHOR_NAME, authorName) .replaceAll(AUTHOR_NAME, escapeDollar(authorName))
.replaceAll(FILE_NAME, fileName) .replaceAll(FILE_NAME, escapeDollar(fileName))
.replaceAll(TYPIST_NAME, typistName) .replaceAll(TYPIST_NAME, escapeDollar(typistName))
.replaceAll(PRIMARY_ADMIN_NAME, adminName); .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(adminName));
// OMDS_IS-380 Dictation Workflow完了通知 [U-117]  をTypistには送信しないようにしたいの対応のため送信先からtypistEmailを削除 2024年8月7日
const to = [authorEmail].filter((x): x is string => x !== null);
if (to.length === 0) {
this.logger.log('There is no email recipient.');
return;
}
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(context, to, [], this.mailFrom, subject, text, html);
context,
[authorEmail, typistEmail].filter((x): x is string => x !== null), // authorEmailがnullの場合は除外する
[],
this.mailFrom,
subject,
text,
html,
);
} finally { } finally {
this.logger.log( this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`, `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`,
@ -1147,20 +1166,19 @@ export class SendGridService {
if (!dealerAccountName) { if (!dealerAccountName) {
html = this.templateU118NoParentHtml.replaceAll( html = this.templateU118NoParentHtml.replaceAll(
CUSTOMER_NAME, CUSTOMER_NAME,escapeDollar( customerAccountName),
customerAccountName,
); );
text = this.templateU118NoParentText.replaceAll( text = this.templateU118NoParentText.replaceAll(
CUSTOMER_NAME, CUSTOMER_NAME,
customerAccountName, escapeDollar(customerAccountName),
); );
} else { } else {
html = this.templateU118Html html = this.templateU118Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
text = this.templateU118Text text = this.templateU118Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
} }
// メールを送信する // メールを送信する
@ -1208,19 +1226,19 @@ export class SendGridService {
if (!dealerAccountName) { if (!dealerAccountName) {
html = this.templateU119NoParentHtml.replaceAll( html = this.templateU119NoParentHtml.replaceAll(
CUSTOMER_NAME, CUSTOMER_NAME,
customerAccountName, escapeDollar(customerAccountName),
); );
text = this.templateU119NoParentText.replaceAll( text = this.templateU119NoParentText.replaceAll(
CUSTOMER_NAME, CUSTOMER_NAME,
customerAccountName, escapeDollar(customerAccountName),
); );
} else { } else {
html = this.templateU119Html html = this.templateU119Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
text = this.templateU119Text text = this.templateU119Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
} }
// メールを送信する // メールを送信する
@ -1269,24 +1287,24 @@ export class SendGridService {
if (!dealerAccountName) { if (!dealerAccountName) {
html = this.templateU120NoParentHtml html = this.templateU120NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
text = this.templateU120NoParentText text = this.templateU120NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
} else { } else {
html = this.templateU120Html html = this.templateU120Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
text = this.templateU120Text text = this.templateU120Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
} }
// メールを送信する // メールを送信する
@ -1335,24 +1353,24 @@ export class SendGridService {
if (!dealerAccountName) { if (!dealerAccountName) {
html = this.templateU121NoParentHtml html = this.templateU121NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
text = this.templateU121NoParentText text = this.templateU121NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
} else { } else {
html = this.templateU121Html html = this.templateU121Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
text = this.templateU121Text text = this.templateU121Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, requestTime) .replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, fileName); .replaceAll(FILE_NAME, escapeDollar(fileName));
} }
// メールを送信する // メールを送信する
@ -1435,52 +1453,52 @@ export class SendGridService {
if (!dealerAccountName) { if (!dealerAccountName) {
html = this.templateU122NoParentHtml html = this.templateU122NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) .replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) .replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) .replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) .replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) .replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) .replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) .replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) .replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); .replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
text = this.templateU122NoParentText text = this.templateU122NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) .replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) .replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) .replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) .replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) .replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) .replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) .replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) .replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); .replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
} else { } else {
html = this.templateU122Html html = this.templateU122Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) .replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) .replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) .replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) .replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) .replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) .replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) .replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) .replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); .replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
text = this.templateU122Text text = this.templateU122Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) .replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) .replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) .replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) .replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) .replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) .replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) .replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) .replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); .replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
} }
// メールを送信する // メールを送信する
@ -1525,13 +1543,13 @@ export class SendGridService {
const subject = 'Partner Account Deleted Notification [U-123]'; const subject = 'Partner Account Deleted Notification [U-123]';
const html = this.templateU123Html const html = this.templateU123Html
.replaceAll(CUSTOMER_NAME, partnerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
const text = this.templateU123Text const text = this.templateU123Text
.replaceAll(CUSTOMER_NAME, partnerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, dealerAccountName); .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -1576,15 +1594,15 @@ export class SendGridService {
const url = new URL(this.appDomain).href; const url = new URL(this.appDomain).href;
const html = this.templateU124Html const html = this.templateU124Html
.replaceAll(CUSTOMER_NAME, partnerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
const text = this.templateU124Text const text = this.templateU124Text
.replaceAll(CUSTOMER_NAME, partnerAccountName) .replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) .replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(TOP_URL, url); .replaceAll(TOP_URL, escapeDollar(url));
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -1603,6 +1621,104 @@ export class SendGridService {
} }
} }
/**
* U-125使
* @param context
* @param adminMailaddresses
* @param accountName
* @param licenseQuantity
* @param expirationDay
* @param dealerEmails
* @returns mail with U125
*/
async sendMailWithU125(
context: Context,
adminMailaddresses: string[],
accountName: string,
licenseQuantity: number,
expirationDay: number,
dealerEmails: string[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU125.name}`,
);
try {
const subject = 'Issued Trial License Notification [U-125]';
const url = new URL(this.appDomain).href;
const html = this.templateU125Html
.replaceAll(CUSTOMER_NAME, escapeDollar(accountName))
.replaceAll(LICENSE_QUANTITY, escapeDollar(String(licenseQuantity)))
.replaceAll(EXPIRATION_DATE, escapeDollar(String(expirationDay)))
.replaceAll(TOP_URL, escapeDollar(url));
const text = this.templateU125Text
.replaceAll(CUSTOMER_NAME, escapeDollar(accountName))
.replaceAll(LICENSE_QUANTITY, escapeDollar(String(licenseQuantity)))
.replaceAll(EXPIRATION_DATE, escapeDollar(String(expirationDay)))
.replaceAll(TOP_URL, escapeDollar(url));
// メールを送信する
await this.sendMail(
context,
adminMailaddresses,
dealerEmails,
this.mailFrom,
subject,
text,
html,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU125.name}`,
);
}
}
/**
* U-126使
* @param context
* @param mailaddress
* @param userName
* @param adminMailaddress
* @param adminUserName
* @returns mail with U126
*/
async sendMailWithU126(
context: Context,
mailaddress: string,
userName: string,
adminMailaddress: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU126.name}`,
);
try {
const subject = 'Forced Email Verification Notification [U-126]';
const url = new URL(this.appDomain).href;
const html = this.templateU126Html
.replaceAll(CUSTOMER_NAME, escapeDollar(userName))
.replaceAll(TOP_URL, escapeDollar(url));
const text = this.templateU126Text
.replaceAll(CUSTOMER_NAME, escapeDollar(userName))
.replaceAll(TOP_URL, escapeDollar(url));
// メールを送信する
await this.sendMail(
context,
[mailaddress],
[adminMailaddress],
this.mailFrom,
subject,
text,
html,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU126.name}`,
);
}
}
/** /**
* *
* @param context * @param context
@ -1625,13 +1741,21 @@ export class SendGridService {
): Promise<void> { ): Promise<void> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.sendMail.name}`); this.logger.log(`[IN] [${context.getTrackingId()}] ${this.sendMail.name}`);
try { try {
// toの重複を削除
const uniqueTo = [...new Set(to)];
// ccの重複を削除
let uniqueCc = [...new Set(cc)];
// toとccの重複を削除cc側から削除
uniqueCc = uniqueCc.filter((email) => !uniqueTo.includes(email));
const res = await sendgrid const res = await sendgrid
.send({ .send({
from: { from: {
email: from, email: from,
}, },
to: to.map((v) => ({ email: v })), to: uniqueTo.map((v) => ({ email: v })),
cc: cc.map((v) => ({ email: v })), cc: uniqueCc.map((v) => ({ email: v })),
subject: subject, subject: subject,
text: text, text: text,
html: html, html: html,
@ -1652,3 +1776,9 @@ export class SendGridService {
} }
} }
} }
/**
* $ $$
* @param str -
* @returns
*/
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");

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