Compare commits
13 Commits
release-20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d9a254b63 | ||
|
|
ff5533b647 | ||
|
|
b3845187f6 | ||
|
|
019c818a19 | ||
|
|
1137d826ae | ||
|
|
b529388871 | ||
|
|
aef17893d9 | ||
|
|
4f598b0017 | ||
|
|
b71ec627d7 | ||
|
|
af56f8ccad | ||
|
|
0b451ed62f | ||
|
|
11395279af | ||
|
|
a07cfe51aa |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
environment_building_tools/logfile.log
|
||||
363
azure-pipelines-staging-ph1-enhance.yml
Normal file
363
azure-pipelines-staging-ph1-enhance.yml
Normal 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
|
||||
@ -17,10 +17,6 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$
|
||||
&& apt-get install default-jre -y \
|
||||
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
|
||||
|
||||
|
||||
# Update NPM
|
||||
RUN npm install -g npm
|
||||
|
||||
# Install mob
|
||||
RUN curl -sL install.mob.sh | sh
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ module.exports = {
|
||||
rules: {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/function-component-definition": [
|
||||
"error",
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@ import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssue
|
||||
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
|
||||
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
|
||||
import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice";
|
||||
import licenseTrialIssue from "features/license/licenseTrialIssue/licenseTrialIssueSlice";
|
||||
import searchPartners from "features/license/searchPartner/searchPartnerSlice";
|
||||
import dictation from "features/dictation/dictationSlice";
|
||||
import partner from "features/partner/partnerSlice";
|
||||
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
|
||||
@ -35,6 +37,8 @@ export const store = configureStore({
|
||||
licenseSummary,
|
||||
licenseOrderHistory,
|
||||
partnerLicense,
|
||||
licenseTrialIssue,
|
||||
searchPartners,
|
||||
dictation,
|
||||
partner,
|
||||
typistGroup,
|
||||
|
||||
7
dictation_client/src/assets/images/search.svg
Normal file
7
dictation_client/src/assets/images/search.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 42.949219 37.109375 L 33.898438 28.0625 L 33.007812 28.949219 L 31.476562 27.414062 C 33.183594 24.882812 34.046875 21.945312 34.042969 19.011719 C 34.046875 15.171875 32.578125 11.320312 29.644531 8.390625 C 26.722656 5.464844 22.867188 4 19.019531 4.003906 C 15.179688 4 11.324219 5.46875 8.398438 8.390625 C 5.46875 11.320312 4 15.167969 4.003906 19.011719 C 4 22.851562 5.46875 26.703125 8.394531 29.628906 C 11.328125 32.554688 15.179688 34.019531 19.023438 34.019531 C 21.957031 34.019531 24.902344 33.160156 27.433594 31.453125 L 28.96875 32.988281 L 28.082031 33.871094 L 37.136719 42.917969 C 37.847656 43.636719 38.796875 43.996094 39.738281 43.996094 C 40.671875 43.996094 41.621094 43.636719 42.335938 42.917969 L 42.949219 42.308594 C 43.664062 41.59375 44.027344 40.644531 44.027344 39.707031 C 44.027344 38.769531 43.664062 37.824219 42.949219 37.109375 Z M 19.023438 32.003906 C 15.6875 32.003906 12.359375 30.738281 9.824219 28.199219 C 7.285156 25.667969 6.019531 22.34375 6.019531 19.011719 C 6.019531 15.675781 7.289062 12.351562 9.824219 9.820312 C 12.359375 7.285156 15.6875 6.019531 19.019531 6.019531 C 22.355469 6.019531 25.683594 7.285156 28.21875 9.820312 C 30.757812 12.351562 32.023438 15.675781 32.027344 19.011719 C 32.023438 22.34375 30.757812 25.667969 28.222656 28.199219 C 25.683594 30.738281 22.355469 32.003906 19.023438 32.003906 Z M 28.78125 30.421875 C 29.074219 30.171875 29.367188 29.910156 29.648438 29.628906 C 29.929688 29.351562 30.191406 29.058594 30.445312 28.761719 L 31.820312 30.136719 L 30.15625 31.800781 Z M 41.523438 40.882812 L 40.910156 41.492188 C 40.582031 41.820312 40.164062 41.976562 39.734375 41.980469 C 39.308594 41.980469 38.890625 41.820312 38.5625 41.496094 L 30.9375 33.875 L 33.898438 30.917969 L 41.523438 38.535156 C 41.847656 38.863281 42.007812 39.28125 42.007812 39.707031 C 42.007812 40.136719 41.847656 40.554688 41.523438 40.882812 Z M 41.523438 40.882812 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 25.695312 12.34375 C 23.855469 10.507812 21.433594 9.585938 19.023438 9.585938 C 16.609375 9.585938 14.191406 10.507812 12.351562 12.34375 C 10.511719 14.179688 9.589844 16.601562 9.59375 19.011719 C 9.589844 21.421875 10.511719 23.84375 12.351562 25.675781 C 14.191406 27.511719 16.609375 28.433594 19.019531 28.433594 C 21.433594 28.433594 23.855469 27.511719 25.695312 25.675781 C 27.535156 23.839844 28.453125 21.421875 28.453125 19.011719 C 28.457031 16.601562 27.53125 14.183594 25.695312 12.34375 Z M 24.503906 24.488281 C 22.992188 25.996094 21.011719 26.753906 19.019531 26.75 C 17.03125 26.75 15.050781 25.996094 13.539062 24.488281 C 12.027344 22.976562 11.277344 21 11.277344 19.011719 C 11.277344 17.023438 12.027344 15.042969 13.539062 13.53125 C 15.054688 12.023438 17.03125 11.269531 19.023438 11.265625 C 21.011719 11.269531 22.992188 12.023438 24.503906 13.53125 C 26.015625 15.042969 26.769531 17.023438 26.769531 19.011719 C 26.769531 21 26.015625 22.976562 24.503906 24.488281 Z M 24.503906 24.488281 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@ -47,6 +47,7 @@ export const KEYS_TO_PRESERVE = [
|
||||
"accessToken",
|
||||
"refreshToken",
|
||||
"displayInfo",
|
||||
"filterCriteria",
|
||||
"sortCriteria",
|
||||
];
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ export const getAccountRelationsAsync = createAsyncThunk<
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const dealers = await accountsApi.getDealers();
|
||||
const users = await usersApi.getUsers({
|
||||
const users = await usersApi.getUsers(undefined, undefined, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
return {
|
||||
|
||||
@ -43,6 +43,8 @@ const initialState: DictationState = {
|
||||
direction: DIRECTION.ASC,
|
||||
paramName: SORTABLE_COLUMN.JobNumber,
|
||||
selectedTask: undefined,
|
||||
authorId: "",
|
||||
fileName: "",
|
||||
assignee: {
|
||||
selected: [],
|
||||
pool: [],
|
||||
@ -78,6 +80,14 @@ export const dictationSlice = createSlice({
|
||||
const { paramName } = action.payload;
|
||||
state.apps.paramName = paramName;
|
||||
},
|
||||
changeAuthorId: (state, action: PayloadAction<{ authorId: string }>) => {
|
||||
const { authorId } = action.payload;
|
||||
state.apps.authorId = authorId;
|
||||
},
|
||||
changeFileName: (state, action: PayloadAction<{ fileName: string }>) => {
|
||||
const { fileName } = action.payload;
|
||||
state.apps.fileName = fileName;
|
||||
},
|
||||
changeSelectedTask: (state, action: PayloadAction<{ task: Task }>) => {
|
||||
const { task } = action.payload;
|
||||
state.apps.selectedTask = task;
|
||||
@ -246,6 +256,8 @@ export const {
|
||||
changeDisplayInfo,
|
||||
changeDirection,
|
||||
changeParamName,
|
||||
changeAuthorId,
|
||||
changeFileName,
|
||||
changeSelectedTask,
|
||||
changeAssignee,
|
||||
changeBackupTaskChecked,
|
||||
|
||||
@ -35,6 +35,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
filter?: string;
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
authorId?: string;
|
||||
fileName?: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -43,7 +45,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/listTasksAsync", async (args, thunkApi) => {
|
||||
const { limit, offset, filter, direction, paramName } = args;
|
||||
const { limit, offset, filter, direction, paramName, authorId, fileName } =
|
||||
args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -60,6 +63,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
filter,
|
||||
direction,
|
||||
paramName,
|
||||
authorId,
|
||||
fileName,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
@ -80,6 +85,136 @@ export const listTasksAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const getTaskFiltersAsync = createAsyncThunk<
|
||||
{
|
||||
authorId?: string;
|
||||
fileName?: string;
|
||||
},
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/getTaskFiltersAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
const usertaskfilter = await usersApi.getTaskFilter({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const { authorId, fileName } = usertaskfilter.data;
|
||||
return { authorId, fileName };
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const updateTaskFiltersAsync = createAsyncThunk<
|
||||
{
|
||||
/** empty */
|
||||
},
|
||||
{
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/updateTaskFiltersAsync", async (args, thunkApi) => {
|
||||
const { filterConditionAuthorId, filterConditionFileName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
return await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const updateSortColumnAsync = createAsyncThunk<
|
||||
{
|
||||
/** empty */
|
||||
},
|
||||
{
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/updateSortColumnAsync", async (args, thunkApi) => {
|
||||
const { direction, paramName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
return await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getSortColumnAsync = createAsyncThunk<
|
||||
{
|
||||
direction: DirectionType;
|
||||
@ -280,6 +415,8 @@ export const playbackAsync = createAsyncThunk<
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -288,7 +425,13 @@ export const playbackAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/playbackAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName } = args;
|
||||
const {
|
||||
audioFileId,
|
||||
direction,
|
||||
paramName,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
} = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -305,6 +448,12 @@ export const playbackAsync = createAsyncThunk<
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await tasksApi.checkout(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
@ -387,6 +536,8 @@ export const cancelAsync = createAsyncThunk<
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
isTypist: boolean;
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -395,7 +546,14 @@ export const cancelAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/cancelAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName, isTypist } = args;
|
||||
const {
|
||||
audioFileId,
|
||||
direction,
|
||||
paramName,
|
||||
isTypist,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
} = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -406,15 +564,25 @@ export const cancelAsync = createAsyncThunk<
|
||||
const tasksApi = new TasksApi(config);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
// ユーザーがタイピストである場合に、ソート条件を保存する
|
||||
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
|
||||
if (isTypist) {
|
||||
await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
direction,
|
||||
paramName,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await tasksApi.cancel(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
@ -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<
|
||||
TasksResponse,
|
||||
{
|
||||
@ -480,6 +735,8 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
|
||||
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
|
||||
DIRECTION.DESC,
|
||||
SORTABLE_COLUMN.Status,
|
||||
undefined, // backupポップアップ表示時には検索条件は未指定
|
||||
undefined, // backupポップアップ表示時には検索条件は未指定
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
|
||||
@ -72,6 +72,12 @@ export const selectDirection = (state: RootState) =>
|
||||
export const selectParamName = (state: RootState) =>
|
||||
state.dictation.apps.paramName;
|
||||
|
||||
export const selectAuthorId = (state: RootState) =>
|
||||
state.dictation.apps.authorId;
|
||||
|
||||
export const selectFilename = (state: RootState) =>
|
||||
state.dictation.apps.fileName;
|
||||
|
||||
export const selectSelectedTask = (state: RootState) =>
|
||||
state.dictation.apps.selectedTask;
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ export interface Apps {
|
||||
displayInfo: DisplayInfoType;
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
authorId: string;
|
||||
fileName: string;
|
||||
selectedTask?: Task;
|
||||
selectedFileTask?: Task;
|
||||
assignee: {
|
||||
|
||||
@ -7,3 +7,8 @@ export const STATUS = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ORDER_CANCELED: "Order Canceled",
|
||||
} as const;
|
||||
|
||||
export const LICENSE_TYPE = {
|
||||
NORMAL: "NORMAL",
|
||||
TRIAL: "TRIAL",
|
||||
} as const;
|
||||
|
||||
@ -3,7 +3,12 @@ import type { RootState } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import { AccountsApi, LicensesApi } from "../../../api/api";
|
||||
import {
|
||||
AccountsApi,
|
||||
LicensesApi,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
} from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
import { OrderHistoryView } from "./types";
|
||||
@ -15,6 +20,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
// パラメータ
|
||||
limit: number;
|
||||
offset: number;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -23,7 +29,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => {
|
||||
const { limit, offset } = args;
|
||||
const { limit, offset, selectedRow } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
@ -33,7 +39,6 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const { selectedRow } = state.partnerLicense.apps;
|
||||
let accountId = 0;
|
||||
let companyName = "";
|
||||
// 他の画面から指定されていない場合はログインアカウントのidを取得する
|
||||
@ -46,7 +51,9 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
companyName = getMyAccountResponse.data.account.companyName;
|
||||
} else {
|
||||
accountId = selectedRow.accountId;
|
||||
companyName = selectedRow.companyName;
|
||||
// パートナーライセンスとパートナー検索で型が異なるため、型ガードで推論させる
|
||||
if ("companyName" in selectedRow) companyName = selectedRow.companyName;
|
||||
if ("name" in selectedRow) companyName = selectedRow.name;
|
||||
}
|
||||
|
||||
const res = await accountsApi.getOrderHistories(
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
AccountsApi,
|
||||
GetCompanyNameResponse,
|
||||
GetLicenseSummaryResponse,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
UpdateRestrictionStatusRequest,
|
||||
} from "../../../api/api";
|
||||
@ -17,7 +18,7 @@ export const getLicenseSummaryAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetLicenseSummaryResponse,
|
||||
// 引数
|
||||
{ selectedRow?: PartnerLicenseInfo },
|
||||
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
@ -73,7 +74,7 @@ export const getCompanyNameAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetCompanyNameResponse,
|
||||
// 引数
|
||||
{ selectedRow?: PartnerLicenseInfo },
|
||||
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export const ISSUED_TRIAL_LICENSE_QUANTITY = 10;
|
||||
export const TRIAL_LICENSE_EXPIRATION_DAY = 30;
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
export * from "./licenseTrialIssueSlice";
|
||||
export * from "./constants";
|
||||
@ -0,0 +1,60 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
|
||||
import { LicenseTrialIssueState } from "./state";
|
||||
import { issueTrialLicenseAsync } from "./operations";
|
||||
import {
|
||||
TRIAL_LICENSE_EXPIRATION_DAY,
|
||||
ISSUED_TRIAL_LICENSE_QUANTITY,
|
||||
} from "./constants";
|
||||
|
||||
const initialState: LicenseTrialIssueState = {
|
||||
apps: {
|
||||
isLoading: false,
|
||||
expirationDate: "",
|
||||
quantity: ISSUED_TRIAL_LICENSE_QUANTITY,
|
||||
},
|
||||
};
|
||||
export const licenseTrialIssueSlice = createSlice({
|
||||
name: "licenseTrialIssue",
|
||||
initialState,
|
||||
reducers: {
|
||||
cleanupApps: (state) => {
|
||||
state.apps = initialState.apps;
|
||||
},
|
||||
setExpirationDate: (state) => {
|
||||
// 有効期限を設定
|
||||
const currentDate = new Date();
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(currentDate.getDate() + TRIAL_LICENSE_EXPIRATION_DAY);
|
||||
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
|
||||
const expirationDateLocal = convertLocalToUTCDate(expiryDate);
|
||||
const expirationDateWithoutTime = new Date(
|
||||
expirationDateLocal.getFullYear(),
|
||||
expirationDateLocal.getMonth(),
|
||||
expirationDateLocal.getDate()
|
||||
);
|
||||
const expirationYear = expirationDateWithoutTime.getFullYear();
|
||||
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
|
||||
const expirationDay = expirationDateWithoutTime.getDate();
|
||||
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay} (${TRIAL_LICENSE_EXPIRATION_DAY})`;
|
||||
|
||||
state.apps.expirationDate = formattedExpirationDate;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(issueTrialLicenseAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(issueTrialLicenseAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(issueTrialLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { cleanupApps, setExpirationDate } =
|
||||
licenseTrialIssueSlice.actions;
|
||||
|
||||
export default licenseTrialIssueSlice.reducer;
|
||||
@ -0,0 +1,84 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import {
|
||||
LicensesApi,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
} from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
|
||||
export const issueTrialLicenseAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/issueTrialLicenseAsync", async (args, thunkApi) => {
|
||||
const { selectedRow } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const licensesApi = new LicensesApi(config);
|
||||
|
||||
try {
|
||||
if (!selectedRow) {
|
||||
// アカウントが選択されていない場合はエラーとする。
|
||||
const errorMessage = getTranslationID(
|
||||
"trialLicenseIssuePopupPage.message.accountNotSelected"
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// トライアルライセンス発行処理を実行
|
||||
await licensesApi.issueTrialLicenses(
|
||||
{
|
||||
issuedAccount: selectedRow.accountId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
const errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.isLoading;
|
||||
|
||||
export const selectExpirationDate = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.expirationDate;
|
||||
|
||||
export const selectNumberOfLicenses = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.quantity;
|
||||
@ -0,0 +1,9 @@
|
||||
export interface LicenseTrialIssueState {
|
||||
apps: Apps;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
expirationDate: string;
|
||||
quantity: number;
|
||||
}
|
||||
@ -25,6 +25,7 @@ const initialState: PartnerLicensesState = {
|
||||
tier: 0,
|
||||
companyName: "",
|
||||
stockLicense: 0,
|
||||
allocatedLicense: 0,
|
||||
issuedRequested: 0,
|
||||
shortage: 0,
|
||||
issueRequesting: 0,
|
||||
@ -39,6 +40,9 @@ const initialState: PartnerLicensesState = {
|
||||
hierarchicalElements: [],
|
||||
isLoading: true,
|
||||
selectedRow: undefined,
|
||||
isLicenseOrderHistoryOpen: false,
|
||||
isViewDetailsOpen: false,
|
||||
isSearchPopupOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -88,6 +92,24 @@ export const partnerLicenseSlice = createSlice({
|
||||
state.apps.limit = limit;
|
||||
state.apps.offset = offset;
|
||||
},
|
||||
setIsLicenseOrderHistoryOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
|
||||
},
|
||||
setIsViewDetailsOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isViewDetailsOpen = action.payload.value;
|
||||
},
|
||||
setIsSearchPopupOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isSearchPopupOpen = action.payload.value;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getMyAccountAsync.pending, (state) => {
|
||||
@ -131,6 +153,9 @@ export const {
|
||||
clearHierarchicalElement,
|
||||
changeSelectedRow,
|
||||
savePageInfo,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
setIsSearchPopupOpen,
|
||||
} = partnerLicenseSlice.actions;
|
||||
|
||||
export default partnerLicenseSlice.reducer;
|
||||
|
||||
@ -30,3 +30,10 @@ export const selectCurrentPage = (state: RootState) => {
|
||||
};
|
||||
export const selectSelectedRow = (state: RootState) =>
|
||||
state.partnerLicense.apps.selectedRow;
|
||||
|
||||
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isLicenseOrderHistoryOpen;
|
||||
export const selectIsViewDetailsOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isViewDetailsOpen;
|
||||
export const selectIsSearchPopupOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isSearchPopupOpen;
|
||||
|
||||
@ -20,6 +20,9 @@ export interface Apps {
|
||||
hierarchicalElements: HierarchicalElement[];
|
||||
isLoading: boolean;
|
||||
selectedRow?: PartnerLicenseInfo;
|
||||
isLicenseOrderHistoryOpen: boolean;
|
||||
isViewDetailsOpen: boolean;
|
||||
isSearchPopupOpen: boolean;
|
||||
}
|
||||
|
||||
export interface HierarchicalElement {
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
export * from "./searchPartnerSlice";
|
||||
@ -0,0 +1,96 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import type { RootState } from "../../../app/store";
|
||||
import { getTranslationID } from "../../../translation";
|
||||
import { openSnackbar } from "../../ui/uiSlice";
|
||||
import { AccountsApi, SearchPartner, PartnerHierarchy } from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
|
||||
export const searchPartnersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
SearchPartner[],
|
||||
// 引数
|
||||
{
|
||||
companyName?: string;
|
||||
accountId?: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/searchPartners", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { companyName, accountId } = args;
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
try {
|
||||
const searchPartnerResponse = await accountsApi.searchPartners(
|
||||
companyName,
|
||||
accountId,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
return searchPartnerResponse.data.searchResult;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getPartnerHierarchy = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
PartnerHierarchy[],
|
||||
// 引数
|
||||
{
|
||||
accountId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/getPartnerHierarchy", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { accountId } = args;
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
try {
|
||||
const partnerHierarchyResponse = await accountsApi.getPartnerHierarchy(
|
||||
accountId,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
return partnerHierarchyResponse.data.accountHierarchy;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { SearchPartner } from "../../../api";
|
||||
import { SearchPartnerState } from "./state";
|
||||
import { searchPartnersAsync, getPartnerHierarchy } from "./operations";
|
||||
|
||||
const initialState: SearchPartnerState = {
|
||||
domain: {
|
||||
searchResult: [],
|
||||
partnerHierarchy: [],
|
||||
},
|
||||
apps: {
|
||||
isLoading: false,
|
||||
selectedRow: undefined,
|
||||
isLicenseOrderHistoryOpen: false,
|
||||
isViewDetailsOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const searchPartnersSlice = createSlice({
|
||||
name: "searchPartners",
|
||||
initialState,
|
||||
reducers: {
|
||||
changeSelectedRow: (
|
||||
state,
|
||||
action: PayloadAction<{ value?: SearchPartner }>
|
||||
) => {
|
||||
const { value } = action.payload;
|
||||
state.apps.selectedRow = value;
|
||||
},
|
||||
setIsLicenseOrderHistoryOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
|
||||
},
|
||||
setIsViewDetailsOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isViewDetailsOpen = action.payload.value;
|
||||
},
|
||||
cleanupSearchResult: (state) => {
|
||||
state.domain.searchResult = initialState.domain.searchResult;
|
||||
},
|
||||
cleanupPartnerHierarchy: (state) => {
|
||||
state.domain.partnerHierarchy = initialState.domain.partnerHierarchy;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(searchPartnersAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(searchPartnersAsync.fulfilled, (state, action) => {
|
||||
state.domain.searchResult = action.payload;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(searchPartnersAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.fulfilled, (state, action) => {
|
||||
state.domain.partnerHierarchy = action.payload;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
export const {
|
||||
changeSelectedRow,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
cleanupSearchResult,
|
||||
cleanupPartnerHierarchy,
|
||||
} = searchPartnersSlice.actions;
|
||||
|
||||
export default searchPartnersSlice.reducer;
|
||||
@ -0,0 +1,14 @@
|
||||
import { RootState } from "../../../app/store";
|
||||
|
||||
export const selectSearchResult = (state: RootState) =>
|
||||
state.searchPartners.domain.searchResult;
|
||||
export const selectPartnerHierarchy = (state: RootState) =>
|
||||
state.searchPartners.domain.partnerHierarchy;
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.searchPartners.apps.isLoading;
|
||||
export const selectSelectedRow = (state: RootState) =>
|
||||
state.searchPartners.apps.selectedRow;
|
||||
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
|
||||
state.searchPartners.apps.isLicenseOrderHistoryOpen;
|
||||
export const selectIsViewDetailsOpen = (state: RootState) =>
|
||||
state.searchPartners.apps.isViewDetailsOpen;
|
||||
18
dictation_client/src/features/license/searchPartner/state.ts
Normal file
18
dictation_client/src/features/license/searchPartner/state.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { SearchPartner, PartnerHierarchy } from "../../../api/api";
|
||||
|
||||
export interface SearchPartnerState {
|
||||
domain: Domain;
|
||||
apps: Apps;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
searchResult: SearchPartner[];
|
||||
partnerHierarchy: PartnerHierarchy[];
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
selectedRow?: SearchPartner;
|
||||
isLicenseOrderHistoryOpen: boolean;
|
||||
isViewDetailsOpen: boolean;
|
||||
}
|
||||
@ -18,7 +18,7 @@ export const listUsersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetUsersResponse,
|
||||
// 引数
|
||||
void,
|
||||
undefined | { userInputUserName?: string; userInputEmail?: string },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
@ -33,9 +33,11 @@ export const listUsersAsync = createAsyncThunk<
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
const userInputUserName = args?.userInputUserName;
|
||||
const userInputEmail = args?.userInputEmail;
|
||||
|
||||
try {
|
||||
const res = await usersApi.getUsers({
|
||||
const res = await usersApi.getUsers(userInputUserName, userInputEmail, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
@ -500,6 +502,72 @@ export const deleteUserAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const confirmUserForceAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
// 引数
|
||||
{
|
||||
userId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/confirmUserForceAsync", async (args, thunkApi) => {
|
||||
const { userId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
await usersApi.confirmUserForce(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
// ユーザーが既に認証済みのため、強制認証不可
|
||||
if (error.code === "E010202") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.alreadyEmailVerifiedError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const importUsersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
selectDirection,
|
||||
changeParamName,
|
||||
changeDirection,
|
||||
changeAuthorId,
|
||||
changeFileName,
|
||||
changeSelectedTask,
|
||||
openFilePropertyInfo,
|
||||
SortableColumnType,
|
||||
@ -32,10 +34,16 @@ import {
|
||||
selectIsLoading,
|
||||
playbackAsync,
|
||||
cancelAsync,
|
||||
reopenAsync,
|
||||
PRIORITY,
|
||||
deleteTaskAsync,
|
||||
isSortableColumnType,
|
||||
isDirectionType,
|
||||
getTaskFiltersAsync,
|
||||
selectAuthorId,
|
||||
selectFilename,
|
||||
updateTaskFiltersAsync,
|
||||
updateSortColumnAsync,
|
||||
} from "features/dictation";
|
||||
import { getTranslationID } from "translation";
|
||||
import { Task } from "api/api";
|
||||
@ -54,6 +62,7 @@ import { DisPlayInfo } from "./displayInfo";
|
||||
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
|
||||
import { BackupPopup } from "./backupPopup";
|
||||
import { FilePropertyPopup } from "./filePropertyPopup";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
|
||||
const DictationPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -100,10 +109,18 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
const [filterFinished, setFilterFinished] = useState(true);
|
||||
const [filterBackup, setFilterBackup] = useState(false);
|
||||
|
||||
// 検索条件の入力値
|
||||
const [filterConditionAuthorId, setFilterConditionAuthorId] = useState("");
|
||||
const [filterConditionFileName, setFilterConditionFileName] = useState("");
|
||||
|
||||
// ソート対象カラム
|
||||
const sortableParamName = useSelector(selectParamName);
|
||||
const sortDirection = useSelector(selectDirection);
|
||||
|
||||
// task_filtersテーブルの検索条件
|
||||
const authorId = useSelector(selectAuthorId);
|
||||
const fileName = useSelector(selectFilename);
|
||||
|
||||
const tasks = useSelector(selectTasks);
|
||||
const total = useSelector(selectTotal);
|
||||
const totalPage = useSelector(selectTotalPage);
|
||||
@ -127,6 +144,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -140,6 +159,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getLastPage = useCallback(() => {
|
||||
@ -158,6 +179,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -172,6 +195,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getPrevPage = useCallback(() => {
|
||||
@ -190,6 +215,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -204,6 +231,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getNextPage = useCallback(() => {
|
||||
@ -222,6 +251,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -236,6 +267,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const updateSortColumn = useCallback(
|
||||
@ -268,6 +301,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: currentDirection,
|
||||
paramName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -282,6 +317,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
@ -331,6 +368,19 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
hasFinished,
|
||||
hasBackup
|
||||
);
|
||||
|
||||
// フィルターの状態をローカルストレージに保存する
|
||||
localStorage.setItem(
|
||||
"filterCriteria",
|
||||
JSON.stringify({
|
||||
Uploaded: hasUploaded,
|
||||
InProgress: hasInProgress,
|
||||
Pending: hasPending,
|
||||
Finished: hasFinished,
|
||||
Backup: hasBackup,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
@ -338,12 +388,14 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
dispatch(listTypistGroupsAsync());
|
||||
},
|
||||
[dispatch, sortDirection, sortableParamName]
|
||||
[dispatch, sortDirection, sortableParamName, authorId, fileName]
|
||||
);
|
||||
|
||||
const onPlayBack = useCallback(
|
||||
@ -359,6 +411,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
audioFileId,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
filterConditionAuthorId: authorId,
|
||||
filterConditionFileName: fileName,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
@ -378,14 +432,15 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
dispatch(listTypistGroupsAsync());
|
||||
|
||||
const url = `${
|
||||
import.meta.env.VITE_DESK_TOP_APP_SCHEME
|
||||
}:playback?audioId=${audioFileId}`;
|
||||
const url = `${import.meta.env.VITE_DESK_TOP_APP_SCHEME
|
||||
}:playback?audioId=${audioFileId}`;
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
document.body.appendChild(a);
|
||||
@ -402,6 +457,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
@ -423,6 +480,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -437,6 +496,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
@ -454,6 +515,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
isTypist,
|
||||
filterConditionAuthorId: authorId,
|
||||
filterConditionFileName: fileName,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
@ -471,6 +534,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -487,6 +552,67 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
isTypist,
|
||||
sortDirection,
|
||||
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,
|
||||
]
|
||||
);
|
||||
@ -516,6 +642,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -530,9 +658,28 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
const onChangeFilterConditionFileName = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterConditionFileName(e.target.value.trimStart());
|
||||
},
|
||||
[setFilterConditionFileName]
|
||||
);
|
||||
|
||||
const onChangeFilterConditionAuthorId = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 先頭に%が入力されるとWAFのルールでブロックされてしまう。
|
||||
// Authorの登録時に「_」以外の記号は許可されていないため、「_」と半角英数字以外の文字は除去。
|
||||
const correctAuthorId = e.target.value.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
setFilterConditionAuthorId(correctAuthorId);
|
||||
},
|
||||
[setFilterConditionAuthorId]
|
||||
);
|
||||
|
||||
const sortIconClass = (
|
||||
currentParam: SortableColumnType,
|
||||
currentDirection: DirectionType,
|
||||
@ -575,6 +722,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -590,10 +739,68 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
const requestSearch = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const { meta: taskFilterMeta } = await dispatch(
|
||||
updateTaskFiltersAsync({
|
||||
filterConditionFileName,
|
||||
filterConditionAuthorId,
|
||||
})
|
||||
);
|
||||
const { meta: sortCriteriaMeta } = await dispatch(
|
||||
updateSortColumnAsync({
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
})
|
||||
);
|
||||
if (
|
||||
taskFilterMeta.requestStatus === "fulfilled" &&
|
||||
sortCriteriaMeta.requestStatus === "fulfilled"
|
||||
) {
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
dispatch(changeAuthorId({ authorId: filterConditionAuthorId }));
|
||||
dispatch(changeFileName({ fileName: filterConditionFileName }));
|
||||
// 検索した条件でタスク一覧を取得する
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
offset: 0,
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId: filterConditionAuthorId,
|
||||
fileName: filterConditionFileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
filterBackup,
|
||||
filterFinished,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
]
|
||||
);
|
||||
|
||||
// 初回読み込み処理
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -609,21 +816,73 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
|
||||
dispatch(changeDisplayInfo({ column: displayInfo }));
|
||||
|
||||
const filter = getFilter(true, true, true, true, false);
|
||||
// フィルター状態をローカルストレージから取得する
|
||||
const filterValue = localStorage.getItem("filterCriteria");
|
||||
|
||||
const { meta, payload } = await dispatch(getSortColumnAsync());
|
||||
let filter: string | undefined;
|
||||
if (filterValue) {
|
||||
const parsedFilter = JSON.parse(filterValue);
|
||||
setFilterUploaded(parsedFilter.Uploaded);
|
||||
setFilterInProgress(parsedFilter.InProgress);
|
||||
setFilterPending(parsedFilter.Pending);
|
||||
setFilterFinished(parsedFilter.Finished);
|
||||
setFilterBackup(parsedFilter.Backup);
|
||||
|
||||
filter = getFilter(
|
||||
parsedFilter.Uploaded,
|
||||
parsedFilter.InProgress,
|
||||
parsedFilter.Pending,
|
||||
parsedFilter.Finished,
|
||||
parsedFilter.Backup
|
||||
);
|
||||
} else {
|
||||
filter = getFilter(true, true, true, true, false);
|
||||
localStorage.setItem(
|
||||
"filterCriteria",
|
||||
JSON.stringify({
|
||||
Uploaded: true,
|
||||
InProgress: true,
|
||||
Pending: true,
|
||||
Finished: true,
|
||||
Backup: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
// タスクフィルター条件
|
||||
const { meta: taskFilterMeta, payload: taskfilterPayload } =
|
||||
await dispatch(getTaskFiltersAsync());
|
||||
let payloadAuthorId: string | undefined;
|
||||
let payloadFileName: string | undefined;
|
||||
if (
|
||||
meta.requestStatus === "fulfilled" &&
|
||||
payload &&
|
||||
!("error" in payload)
|
||||
taskFilterMeta.requestStatus === "fulfilled" &&
|
||||
taskfilterPayload &&
|
||||
!("error" in taskfilterPayload)
|
||||
) {
|
||||
payloadAuthorId = taskfilterPayload.authorId ?? "";
|
||||
payloadFileName = taskfilterPayload.fileName ?? "";
|
||||
|
||||
dispatch(changeAuthorId({ authorId: payloadAuthorId }));
|
||||
dispatch(changeFileName({ fileName: payloadFileName }));
|
||||
// 初回表示時に検索フォームにtask_filtersテーブルの値を設定する。
|
||||
setFilterConditionAuthorId(payloadAuthorId);
|
||||
setFilterConditionFileName(payloadFileName);
|
||||
}
|
||||
|
||||
// ソート条件
|
||||
const { meta: sortCriteriaMeta, payload: sortCriteriaPayload } =
|
||||
await dispatch(getSortColumnAsync());
|
||||
let direction: DirectionType = "ASC";
|
||||
let paramName: SortableColumnType = "JOB_NUMBER";
|
||||
if (
|
||||
sortCriteriaMeta.requestStatus === "fulfilled" &&
|
||||
sortCriteriaPayload &&
|
||||
!("error" in sortCriteriaPayload)
|
||||
) {
|
||||
// ソート情報をローカルストレージから取得する
|
||||
const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
|
||||
let direction: DirectionType;
|
||||
let paramName: SortableColumnType;
|
||||
if (sortColumnValue === "") {
|
||||
direction = payload.direction;
|
||||
paramName = payload.paramName;
|
||||
direction = sortCriteriaPayload.direction;
|
||||
paramName = sortCriteriaPayload.paramName;
|
||||
} else {
|
||||
// ソート情報をDirectionとParamNameに分割する
|
||||
const sortColumn = sortColumnValue?.split(",");
|
||||
@ -634,15 +893,18 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
// 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
|
||||
direction = isDirectionType(localStorageDirection)
|
||||
? localStorageDirection
|
||||
: payload.direction;
|
||||
: sortCriteriaPayload.direction;
|
||||
paramName = isSortableColumnType(localStorageParamName)
|
||||
? localStorageParamName
|
||||
: payload.paramName;
|
||||
: sortCriteriaPayload.paramName;
|
||||
|
||||
dispatch(changeDirection({ direction }));
|
||||
dispatch(changeParamName({ paramName }));
|
||||
}
|
||||
}
|
||||
|
||||
// タスク一覧を取得する
|
||||
if (isDirectionType(direction) && isSortableColumnType(paramName)) {
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
@ -650,6 +912,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction,
|
||||
paramName,
|
||||
authorId: payloadAuthorId,
|
||||
fileName: payloadFileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -682,125 +946,171 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
<section className={styles.dictation}>
|
||||
<div>
|
||||
<DisPlayInfo />
|
||||
<ul className={styles.tableFilter}>
|
||||
<li>{t(getTranslationID("dictationPage.label.filter"))}:</li>
|
||||
<ul className={styles.menuAction}>
|
||||
<li>
|
||||
<label htmlFor="uploaded">
|
||||
<input
|
||||
id="uploaded"
|
||||
type="checkbox"
|
||||
value="flUploaded"
|
||||
className={styles.formCheck}
|
||||
checked={filterUploaded}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterUploaded(e.target.checked);
|
||||
updateFilter(
|
||||
e.target.checked,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.uploaded"))}
|
||||
</label>
|
||||
<ul className={styles.tableFilter}>
|
||||
<li>
|
||||
{t(getTranslationID("dictationPage.label.filter"))}:
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="uploaded">
|
||||
<input
|
||||
id="uploaded"
|
||||
type="checkbox"
|
||||
value="flUploaded"
|
||||
className={styles.formCheck}
|
||||
checked={filterUploaded}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterUploaded(e.target.checked);
|
||||
updateFilter(
|
||||
e.target.checked,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.uploaded"))}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="inProgress">
|
||||
<input
|
||||
id="inProgress"
|
||||
type="checkbox"
|
||||
value="flInProgress"
|
||||
className={styles.formCheck}
|
||||
checked={filterInProgress}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterInProgress(e.target.checked);
|
||||
updateFilter(
|
||||
filterUploaded,
|
||||
e.target.checked,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID("dictationPage.label.inProgress")
|
||||
)}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="pending">
|
||||
<input
|
||||
id="pending"
|
||||
type="checkbox"
|
||||
value="flPending"
|
||||
className={styles.formCheck}
|
||||
checked={filterPending}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterPending(e.target.checked);
|
||||
updateFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
e.target.checked,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.pending"))}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="finished">
|
||||
<input
|
||||
id="finished"
|
||||
type="checkbox"
|
||||
value="flFinished"
|
||||
className={styles.formCheck}
|
||||
checked={filterFinished}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterFinished(e.target.checked);
|
||||
updateFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
e.target.checked,
|
||||
filterBackup
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.finished"))}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="backup">
|
||||
<input
|
||||
id="backup"
|
||||
type="checkbox"
|
||||
value="flBackup"
|
||||
className={styles.formCheck}
|
||||
checked={filterBackup}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => {
|
||||
setFilterBackup(e.target.checked);
|
||||
updateFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.backup"))}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="inProgress">
|
||||
<li className={styles.floatRight}>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<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
|
||||
);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("dictationPage.label.fileName")
|
||||
)}
|
||||
value={filterConditionFileName}
|
||||
onChange={(e) => onChangeFilterConditionFileName(e)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{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
|
||||
);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("dictationPage.label.authorId")
|
||||
)}
|
||||
value={filterConditionAuthorId}
|
||||
onChange={(e) => onChangeFilterConditionAuthorId(e)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{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>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles.menuLink} ${!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.search"))}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={`${styles.table} ${styles.dictation}`}>
|
||||
<tr className={styles.tableHeader}>
|
||||
@ -1227,7 +1537,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
<a
|
||||
className={
|
||||
x.status !== STATUS.UPLOADED ||
|
||||
!(isAdmin || isAuthor)
|
||||
!(isAdmin || isAuthor)
|
||||
? styles.isDisable
|
||||
: ""
|
||||
}
|
||||
@ -1248,7 +1558,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
className={
|
||||
(x.status === STATUS.INPROGRESS ||
|
||||
x.status === STATUS.PENDING) &&
|
||||
(isAdmin || isTypist)
|
||||
(isAdmin || isTypist)
|
||||
? ""
|
||||
: styles.isDisable
|
||||
}
|
||||
@ -1263,14 +1573,35 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</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>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
// タスクのステータスがInprogressまたはPending以外の場合、削除ボタンを活性化する
|
||||
className={
|
||||
isDeletableRole &&
|
||||
x.status !== STATUS.INPROGRESS &&
|
||||
x.status !== STATUS.PENDING
|
||||
x.status !== STATUS.INPROGRESS &&
|
||||
x.status !== STATUS.PENDING
|
||||
? ""
|
||||
: styles.isDisable
|
||||
}
|
||||
@ -1466,18 +1797,16 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
)}`}</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={getFirstPage}
|
||||
>
|
||||
«
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={getPrevPage}
|
||||
>
|
||||
‹
|
||||
@ -1485,22 +1814,20 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
{`${currentPage} of ${totalPage}`}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
}`}
|
||||
onClick={getNextPage}
|
||||
>
|
||||
›
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
}`}
|
||||
onClick={getLastPage}
|
||||
>
|
||||
»
|
||||
@ -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 */}
|
||||
<a
|
||||
onClick={onClickBackup}
|
||||
className={`${styles.menuLink} ${
|
||||
isAdmin ? styles.isActive : ""
|
||||
}`}
|
||||
className={`${styles.menuLink} ${isAdmin ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
<img src={download} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("dictationPage.label.fileBackup"))}
|
||||
|
||||
@ -12,6 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
LIMIT_ORDER_HISORY_NUM,
|
||||
STATUS,
|
||||
LICENSE_TYPE,
|
||||
getLicenseOrderHistoriesAsync,
|
||||
selectCurrentPage,
|
||||
selectIsLoading,
|
||||
@ -25,20 +26,21 @@ import {
|
||||
selectCompanyName,
|
||||
cancelIssueAsync,
|
||||
} from "features/license/licenseOrderHistory";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
import { LicenseOrder, SearchPartner, PartnerLicenseInfo } from "api/api";
|
||||
import undo from "../../assets/images/undo.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface LicenseOrderHistoryProps {
|
||||
onReturn: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
props
|
||||
): JSX.Element => {
|
||||
const { onReturn } = props;
|
||||
const { onReturn, selectedRow } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const total = useSelector(selectTotal);
|
||||
@ -46,7 +48,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
const offset = useSelector(selectOffset);
|
||||
const currentPage = useSelector(selectCurrentPage);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow);
|
||||
// 代行操作用のトークンを取得する
|
||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
@ -64,6 +65,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
selectedRow,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -151,11 +153,15 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
selectedRow,
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, currentPage]);
|
||||
|
||||
const isNotTrialLicense = (license: LicenseOrder) =>
|
||||
license.type !== LICENSE_TYPE.TRIAL;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.wrap} ${delegationAccessToken ? styles.manage : ""}`}
|
||||
@ -208,6 +214,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getTranslationID("orderHistoriesPage.label.issueDate")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("orderHistoriesPage.label.licenseType")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID(
|
||||
@ -229,9 +240,10 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<tr>
|
||||
<td>{x.orderDate}</td>
|
||||
<td>{x.issueDate ? x.issueDate : "-"}</td>
|
||||
<td>{x.issueDate ?? "-"}</td>
|
||||
<td>{x.type}</td>
|
||||
<td>{x.numberOfOrder}</td>
|
||||
<td>{x.poNumber}</td>
|
||||
<td>{x.poNumber ?? "-"}</td>
|
||||
<td>
|
||||
{(() => {
|
||||
switch (x.status) {
|
||||
@ -259,7 +271,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
})()}
|
||||
</td>
|
||||
<td>
|
||||
{!selectedRow && (
|
||||
{!selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
@ -284,7 +296,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow && (
|
||||
{selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
|
||||
@ -14,11 +14,11 @@ import {
|
||||
selectIsLoading,
|
||||
updateRestrictionStatusAsync,
|
||||
} from "features/license/licenseSummary";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import { isAdminUser, isApproveTier } from "features/auth/utils";
|
||||
import { PartnerLicenseInfo, SearchPartner } from "../../api";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import key from "../../assets/images/key.svg";
|
||||
@ -27,19 +27,20 @@ import circle from "../../assets/images/circle.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { CardLicenseActivatePopup } from "./cardLicenseActivatePopup";
|
||||
import { TrialLicenseIssuePopup } from "./trialLicenseIssuePopup";
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import LicenseOrderHistory from "./licenseOrderHistory";
|
||||
|
||||
interface LicenseSummaryProps {
|
||||
onReturn?: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
props
|
||||
): JSX.Element => {
|
||||
const { onReturn } = props;
|
||||
const { onReturn, selectedRow } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const selectedRow = useSelector(selectSelectedRow);
|
||||
// 代行操作用のトークンを取得する
|
||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
@ -49,6 +50,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
|
||||
useState(false);
|
||||
const [isTrialLicenseIssuePopupOpen, setIsTrialLicenseIssuePopupOpen] =
|
||||
useState(false);
|
||||
|
||||
const onlicenseOrderOpen = useCallback(() => {
|
||||
setIslicenseOrderPopupOpen(true);
|
||||
@ -58,6 +61,10 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
setIsCardLicenseActivatePopupOpen(true);
|
||||
}, [setIsCardLicenseActivatePopupOpen]);
|
||||
|
||||
const onTrialLicenseIssueOpen = useCallback(() => {
|
||||
setIsTrialLicenseIssuePopupOpen(true);
|
||||
}, [setIsTrialLicenseIssuePopupOpen]);
|
||||
|
||||
// 呼び出し画面制御関係
|
||||
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
@ -71,6 +78,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
const companyName = useSelector(selectCompanyName);
|
||||
|
||||
const isTier1 = isApproveTier([TIERS.TIER1]);
|
||||
const isTier2 = isApproveTier([TIERS.TIER2]);
|
||||
const isAdmin = isAdminUser();
|
||||
|
||||
useEffect(() => {
|
||||
@ -133,11 +141,21 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isTrialLicenseIssuePopupOpen && (
|
||||
<TrialLicenseIssuePopup
|
||||
onClose={() => {
|
||||
setIsTrialLicenseIssuePopupOpen(false);
|
||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{islicenseOrderHistoryOpen && (
|
||||
<LicenseOrderHistory
|
||||
onReturn={() => {
|
||||
setIsLicenseOrderHistoryOpen(false);
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && (
|
||||
@ -228,6 +246,30 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{/* 第一階層、第二階層の管理者が第五階層アカウントのライセンス情報を見ている場合は、トライアルライセンス注文ボタンを表示 */}
|
||||
{selectedRow &&
|
||||
isAdmin &&
|
||||
selectedRow.tier.toString() === TIERS.TIER5 &&
|
||||
(isTier1 || isTier2) && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onTrialLicenseIssueOpen}
|
||||
>
|
||||
<img
|
||||
src={postAdd}
|
||||
alt=""
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"LicenseSummaryPage.label.issueTrialLicense"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.marginRgt3}>
|
||||
<dl
|
||||
|
||||
@ -1,43 +1,57 @@
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { PartnerLicenseInfo } from "api";
|
||||
import { AppDispatch } from "app/store";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PartnerLicenseInfo } from "api";
|
||||
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { getTranslationID } from "translation";
|
||||
import changeOwnerIcon from "../../assets/images/change_circle.svg";
|
||||
import { isApproveTier } from "../../features/auth/utils";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import { TIERS } from "../../components/auth/constants";
|
||||
import { isApproveTier } from "../../features/auth/utils";
|
||||
import {
|
||||
getPartnerLicenseAsync,
|
||||
ACCOUNTS_VIEW_LIMIT,
|
||||
selectMyAccountInfo,
|
||||
selectTotal,
|
||||
selectOwnPartnerLicense,
|
||||
selectChildrenPartnerLicenses,
|
||||
selectHierarchicalElements,
|
||||
selectTotalPage,
|
||||
selectIsLoading,
|
||||
selectOffset,
|
||||
selectCurrentPage,
|
||||
pushHierarchicalElement,
|
||||
changeSelectedRow,
|
||||
getMyAccountAsync,
|
||||
getPartnerLicenseAsync,
|
||||
popHierarchicalElement,
|
||||
pushHierarchicalElement,
|
||||
spliceHierarchicalElement,
|
||||
savePageInfo,
|
||||
getMyAccountAsync,
|
||||
changeSelectedRow,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
selectChildrenPartnerLicenses,
|
||||
selectCurrentPage,
|
||||
selectHierarchicalElements,
|
||||
selectIsLoading,
|
||||
selectMyAccountInfo,
|
||||
selectOffset,
|
||||
selectOwnPartnerLicense,
|
||||
selectTotal,
|
||||
selectTotalPage,
|
||||
selectSelectedRow,
|
||||
selectIsLicenseOrderHistoryOpen,
|
||||
selectIsViewDetailsOpen,
|
||||
setIsSearchPopupOpen,
|
||||
selectIsSearchPopupOpen,
|
||||
} from "../../features/license/partnerLicense";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
import {
|
||||
selectIsViewDetailsOpen as selectIsViewDetailsInSearchOpen,
|
||||
selectIsLicenseOrderHistoryOpen as selectIsLicenseOrderHistoryInSearchOpen,
|
||||
} from "../../features/license/searchPartner";
|
||||
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
||||
import ChangeOwnerPopup from "./changeOwnerPopup";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import { SearchPartnerPopup } from "./searchPartnerAccountPopup";
|
||||
|
||||
const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -47,9 +61,21 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const [isCardLicenseIssuePopupOpen, setIsCardLicenseIssuePopupOpen] =
|
||||
useState(false);
|
||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
|
||||
|
||||
// パートナーライセンス画面のOrderHistory, ViewDetailsの表示制御
|
||||
const isLicenseOrderHistoryOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryOpen
|
||||
);
|
||||
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
|
||||
|
||||
// パートナー検索ポップアップのOrderHistory, ViewDetailsの表示制御
|
||||
const isLicenseOrderHistoryInSearchOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryInSearchOpen
|
||||
);
|
||||
const isViewDetailsInSearchOpen = useSelector(
|
||||
selectIsViewDetailsInSearchOpen
|
||||
);
|
||||
const isSearchPopupOpen = useSelector(selectIsSearchPopupOpen);
|
||||
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
|
||||
|
||||
// 階層表示用
|
||||
@ -84,6 +110,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
);
|
||||
const hierarchicalElements = useSelector(selectHierarchicalElements);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow) as PartnerLicenseInfo;
|
||||
|
||||
// ページネーション制御用
|
||||
const currentPage = useSelector(selectCurrentPage);
|
||||
@ -136,18 +163,18 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const onClickViewDetails = useCallback(
|
||||
(value?: PartnerLicenseInfo) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
setIsViewDetailsOpen(true);
|
||||
dispatch(setIsViewDetailsOpen({ value: true }));
|
||||
},
|
||||
[dispatch, setIsViewDetailsOpen]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// orderHistoryボタン押下時
|
||||
const onClickOrderHistory = useCallback(
|
||||
(value?: PartnerLicenseInfo) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
setIslicenseOrderHistoryOpen(true);
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
|
||||
},
|
||||
[dispatch, setIslicenseOrderHistoryOpen]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// changeOwnerボタン押下時
|
||||
@ -155,6 +182,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
setIsChangeOwnerPopupOpen(true);
|
||||
}, [setIsChangeOwnerPopupOpen]);
|
||||
|
||||
const onOpenSearchPopup = useCallback(() => {
|
||||
dispatch(setIsSearchPopupOpen({ value: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
// マウント時のみ実行
|
||||
useEffect(() => {
|
||||
dispatch(getMyAccountAsync());
|
||||
@ -176,7 +207,8 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}, [myAccountInfo]);
|
||||
|
||||
// 現在の表示階層に合わせたボタン制御用
|
||||
const [buttonLabel, setButtonLabel] = useState("");
|
||||
const [showOrderHistoryButton, setShowOrderHistoryButton] = useState(false);
|
||||
const [showViewDetailsButton, setShowViewDetailsButton] = useState(false);
|
||||
|
||||
// パンくずリスト用stateに自アカウントを追加
|
||||
useEffect(() => {
|
||||
@ -194,15 +226,17 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
// 表内のボタン表示判定
|
||||
if (hierarchicalElements.length === 1 && ownPartnerLicenseInfo.tier !== 4) {
|
||||
setButtonLabel(
|
||||
t(getTranslationID("partnerLicense.label.orderHistoryButton"))
|
||||
);
|
||||
if (ownPartnerLicenseInfo.tier !== 4) {
|
||||
setShowOrderHistoryButton(true);
|
||||
setShowViewDetailsButton(false);
|
||||
} else if (ownPartnerLicenseInfo.tier === 4) {
|
||||
setButtonLabel(t(getTranslationID("partnerLicense.label.viewDetails")));
|
||||
setShowOrderHistoryButton(true);
|
||||
setShowViewDetailsButton(true);
|
||||
} else {
|
||||
setButtonLabel("");
|
||||
setShowOrderHistoryButton(false);
|
||||
setShowViewDetailsButton(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ownPartnerLicenseInfo]);
|
||||
|
||||
@ -221,6 +255,21 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hierarchicalElements, currentPage]);
|
||||
|
||||
// パートナーライセンス画面からも検索ポップアップからもOrder History/View Detailsが表示されていない時に表示
|
||||
const isVisiblePartnerLicensePage = useMemo(
|
||||
() =>
|
||||
!isLicenseOrderHistoryInSearchOpen &&
|
||||
!isViewDetailsInSearchOpen &&
|
||||
!isLicenseOrderHistoryOpen &&
|
||||
!isViewDetailsOpen,
|
||||
[
|
||||
isLicenseOrderHistoryInSearchOpen,
|
||||
isViewDetailsInSearchOpen,
|
||||
isLicenseOrderHistoryOpen,
|
||||
isViewDetailsOpen,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
||||
@ -238,18 +287,20 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{islicenseOrderHistoryOpen && (
|
||||
{isLicenseOrderHistoryOpen && (
|
||||
<LicenseOrderHistory
|
||||
onReturn={() => {
|
||||
setIslicenseOrderHistoryOpen(false);
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: false }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{isViewDetailsOpen && (
|
||||
<LicenseSummary
|
||||
onReturn={() => {
|
||||
setIsViewDetailsOpen(false);
|
||||
dispatch(setIsViewDetailsOpen({ value: false }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{isChangeOwnerPopupOpen && (
|
||||
@ -259,7 +310,12 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
||||
{isVisiblePartnerSearch() && isSearchPopupOpen && (
|
||||
<SearchPartnerPopup
|
||||
onClose={() => dispatch(setIsSearchPopupOpen({ value: true }))}
|
||||
/>
|
||||
)}
|
||||
{isVisiblePartnerLicensePage && (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
@ -362,6 +418,22 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li className={styles.floatRight}>
|
||||
{isVisiblePartnerSearch() && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive} ${styles.alignRight}`}
|
||||
onClick={onOpenSearchPopup}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("partnerLicense.label.search"))}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<ul className={styles.brCrumbLicense}>
|
||||
{hierarchicalElements.map((value) => (
|
||||
@ -388,6 +460,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<th>
|
||||
{t(getTranslationID("partnerLicense.label.stockLicense"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.allocatedLicense"
|
||||
)
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("partnerLicense.label.issueRequested")
|
||||
@ -413,12 +492,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
? ownPartnerLicenseInfo.stockLicense
|
||||
: "-"}
|
||||
</td>
|
||||
<td>-</td>
|
||||
<td>{ownPartnerLicenseInfo.issuedRequested}</td>
|
||||
<td>
|
||||
<span
|
||||
className={
|
||||
ownPartnerLicenseInfo.shortage > 0 &&
|
||||
ownPartnerLicenseInfo.tier !== 1
|
||||
ownPartnerLicenseInfo.tier !== 1
|
||||
? styles.isAlert
|
||||
: ""
|
||||
}
|
||||
@ -456,6 +536,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<td>{tierNames[value.tier]}</td>
|
||||
<td>{value.accountId}</td>
|
||||
<td>{value.stockLicense}</td>
|
||||
<td>{value.tier === 5 ? value.allocatedLicense : "-"}</td>
|
||||
<td>{value.issuedRequested}</td>
|
||||
<td>
|
||||
<span
|
||||
@ -472,20 +553,38 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${
|
||||
buttonLabel ? styles.isActive : ""
|
||||
}`}
|
||||
className={`${styles.menuLink} ${showOrderHistoryButton ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (ownPartnerLicenseInfo.tier === 4) {
|
||||
onClickViewDetails(value);
|
||||
} else {
|
||||
onClickOrderHistory(value);
|
||||
}
|
||||
onClickOrderHistory(value);
|
||||
}}
|
||||
>
|
||||
{buttonLabel}
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.orderHistoryButton"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* Second button (only if tier is 4) */}
|
||||
{showViewDetailsButton && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${showViewDetailsButton ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onClickViewDetails(value);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.viewDetails"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@ -513,9 +612,8 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage(0);
|
||||
}}
|
||||
className={` ${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
className={` ${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
«
|
||||
</a>
|
||||
@ -524,25 +622,22 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage((currentPage - 2) * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
‹
|
||||
</a>
|
||||
{` ${total !== 0 ? currentPage : 0} of ${
|
||||
total !== 0 ? totalPage : 0
|
||||
} `}
|
||||
{` ${total !== 0 ? currentPage : 0} of ${total !== 0 ? totalPage : 0
|
||||
} `}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
movePage(currentPage * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
›
|
||||
</a>
|
||||
@ -551,11 +646,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage((totalPage - 1) * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={` ${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={` ${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
»
|
||||
</a>
|
||||
@ -584,4 +678,8 @@ const isVisibleChangeOwner = (partnerTier: number) =>
|
||||
(partnerTier.toString() === TIERS.TIER3 ||
|
||||
partnerTier.toString() === TIERS.TIER4);
|
||||
|
||||
const isVisiblePartnerSearch = () =>
|
||||
// 自身が第一階層〜第四階層の場合のみ表示
|
||||
isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]);
|
||||
|
||||
export default PartnerLicense;
|
||||
|
||||
@ -0,0 +1,354 @@
|
||||
import { SearchPartner } from "api";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
changeSelectedRow,
|
||||
cleanupSearchResult,
|
||||
cleanupPartnerHierarchy,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
searchPartnersAsync,
|
||||
selectIsLoading,
|
||||
selectSelectedRow,
|
||||
selectSearchResult,
|
||||
selectPartnerHierarchy,
|
||||
selectIsLicenseOrderHistoryOpen,
|
||||
selectIsViewDetailsOpen,
|
||||
getPartnerHierarchy,
|
||||
} from "features/license/searchPartner";
|
||||
import { setIsSearchPopupOpen } from "features/license/partnerLicense";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface SearchPopupProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SearchPartnerPopup: React.FC<SearchPopupProps> = (props) => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { onClose } = props;
|
||||
const [t] = useTranslation();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow) as SearchPartner;
|
||||
const searchResult = useSelector(selectSearchResult);
|
||||
const partnerHierarchy = useSelector(selectPartnerHierarchy);
|
||||
const [accountId, setAccountId] = useState("");
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const [isBreadcrumbOpen, setIsBreadcrumbOpen] = useState(false);
|
||||
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
|
||||
const isLicenseOrderHistoryOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryOpen
|
||||
);
|
||||
const [popupPosition, setPopupPosition] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const breadcrumbRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// フォームの入力チェック
|
||||
const searchButtonEnabled = useMemo(() => {
|
||||
// 両方入力がない場合はボタンを活性化しない。
|
||||
if (!companyName && !accountId) {
|
||||
return false;
|
||||
}
|
||||
// Company Nameは3文字以上入力がない場合はボタンを活性化しない。
|
||||
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
|
||||
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
|
||||
if (companyName && [...companyName].length <= 2) {
|
||||
return false;
|
||||
}
|
||||
// Account IDは数字ではないまたは0以下の場合はボタンを活性化しない。
|
||||
if (
|
||||
accountId &&
|
||||
(Number.isNaN(Number(accountId)) || Number(accountId) <= 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [companyName, accountId]);
|
||||
|
||||
const requestSearch = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!searchButtonEnabled) return;
|
||||
dispatch(
|
||||
searchPartnersAsync({
|
||||
companyName,
|
||||
accountId: Number(accountId),
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, companyName, accountId, searchButtonEnabled]
|
||||
);
|
||||
|
||||
const handleAccountNameClick = useCallback(
|
||||
async (clickAccountId: number, event: React.MouseEvent) => {
|
||||
// ロード中はなにもしない。
|
||||
if (isLoading) return;
|
||||
event.stopPropagation();
|
||||
// アカウントの階層を取得
|
||||
await dispatch(getPartnerHierarchy({ accountId: clickAccountId }));
|
||||
setPopupPosition({ x: event.clientX, y: event.clientY });
|
||||
setIsBreadcrumbOpen(true);
|
||||
},
|
||||
[dispatch, setPopupPosition, setIsBreadcrumbOpen, isLoading]
|
||||
);
|
||||
|
||||
const closeBreadcrumbPopup = () => {
|
||||
setIsBreadcrumbOpen(false);
|
||||
};
|
||||
|
||||
// ポップアップ外のクリックポップアップ非表示するイベント
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
breadcrumbRef.current &&
|
||||
!breadcrumbRef.current.contains(event.target as Node)
|
||||
) {
|
||||
closeBreadcrumbPopup();
|
||||
}
|
||||
};
|
||||
if (isBreadcrumbOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isBreadcrumbOpen]);
|
||||
|
||||
const tierNames: { [key: number]: string } = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
1: t(getTranslationID("common.label.tier1")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
2: t(getTranslationID("common.label.tier2")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
3: t(getTranslationID("common.label.tier3")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
4: t(getTranslationID("common.label.tier4")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
5: t(getTranslationID("common.label.tier5")),
|
||||
};
|
||||
|
||||
const openViewDetails = useCallback(
|
||||
(value: SearchPartner) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
dispatch(setIsViewDetailsOpen({ value: true }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const openOrderHistory = useCallback(
|
||||
(value?: SearchPartner) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
// ポップアップ閉じたら、検索結果をクリアする
|
||||
dispatch(cleanupSearchResult());
|
||||
dispatch(cleanupPartnerHierarchy());
|
||||
onClose();
|
||||
dispatch(setIsSearchPopupOpen({ value: false }));
|
||||
}, [dispatch, onClose]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div>
|
||||
{isViewDetailsOpen && (
|
||||
<div>
|
||||
<LicenseSummary
|
||||
onReturn={() => dispatch(setIsViewDetailsOpen({ value: false }))}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLicenseOrderHistoryOpen && (
|
||||
<div>
|
||||
<LicenseOrderHistory
|
||||
onReturn={() =>
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: false }))
|
||||
}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.searchModalBox}>
|
||||
<div className={styles.headerContainer}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("searchPartnerAccountPopupPage.label.title"))}
|
||||
</p>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(getTranslationID("partnerLicense.label.name"))}
|
||||
value={companyName}
|
||||
onChange={(e) => setCompanyName(e.target.value.trimStart())}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("partnerLicense.label.accountId")
|
||||
)}
|
||||
value={accountId}
|
||||
onChange={(e) => setAccountId(e.target.value.trimStart())}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
className={`${styles.menuLink} ${!isLoading && searchButtonEnabled ? styles.isActive : ""
|
||||
}`}
|
||||
type="submit"
|
||||
disabled={!searchButtonEnabled || isLoading}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("partnerLicense.label.search"))}
|
||||
</button>
|
||||
<button type="button" onClick={handleModalClose}>
|
||||
<img
|
||||
src={close}
|
||||
className={styles.modalTitleIcon}
|
||||
alt="close"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table
|
||||
className={`${styles.table} ${styles.partner} ${styles.marginBtm3}`}
|
||||
>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.name"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.category"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.accountId"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.country"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.primaryAdmin"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.email"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{"" /** Order History、View Details用の空カラム */}</a>
|
||||
</th>
|
||||
</tr>
|
||||
{searchResult.map((result) => (
|
||||
<tr key={result.accountId}>
|
||||
<td
|
||||
onClick={(event) =>
|
||||
handleAccountNameClick(result.accountId, event)
|
||||
}
|
||||
className={styles.hoverBlue}
|
||||
>
|
||||
{result.name}
|
||||
</td>
|
||||
<td>{tierNames[result.tier]}</td>
|
||||
<td>{result.accountId}</td>
|
||||
<td>{result.country}</td>
|
||||
<td>{result.primaryAdmin}</td>
|
||||
<td>{result.email ?? "-"}</td>
|
||||
<td>
|
||||
<ul className={`${styles.menuAction} ${styles.inTable}`}>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
openOrderHistory(result);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.orderHistoryButton"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
{result.tier === 5 && (
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
openViewDetails(result);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID("partnerLicense.label.viewDetails")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{searchResult.length === 0 && (
|
||||
<p style={{ margin: "10px", textAlign: "center" }}>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{/* ローディングオーバーレイ */}
|
||||
<div
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
className={styles.overlay}
|
||||
>
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={`${styles.icLoading} ${styles.alignCenter}`}
|
||||
alt="Loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isBreadcrumbOpen && (
|
||||
<div
|
||||
ref={breadcrumbRef}
|
||||
className={styles.breadcrumbPopup}
|
||||
style={{ top: popupPosition.y, left: popupPosition.x }}
|
||||
>
|
||||
<ul className={styles.brCrumbPartner}>
|
||||
{partnerHierarchy.map((parent) => (
|
||||
<li key={parent.tier}>
|
||||
<span>{parent.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
issueTrialLicenseAsync,
|
||||
cleanupApps,
|
||||
selectIsLoading,
|
||||
selectNumberOfLicenses,
|
||||
selectExpirationDate,
|
||||
setExpirationDate,
|
||||
} from "features/license/licenseTrialIssue";
|
||||
import styles from "../../styles/app.module.scss";
|
||||
import { getTranslationID } from "../../translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import { SearchPartner, PartnerLicenseInfo } from "../../api";
|
||||
|
||||
interface TrialLicenseIssuePopupProps {
|
||||
onClose: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
|
||||
export const TrialLicenseIssuePopup: React.FC<TrialLicenseIssuePopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { onClose, selectedRow } = props;
|
||||
const { t } = useTranslation();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const numberOfLicenses = useSelector(selectNumberOfLicenses);
|
||||
const expirationDate = useSelector(selectExpirationDate);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
|
||||
dispatch(cleanupApps());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// ポップアップ表示時
|
||||
useEffect(() => {
|
||||
// トライアルライセンスの有効期限を設定。
|
||||
dispatch(setExpirationDate());
|
||||
}, [dispatch]);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
// 発行ボタン押下時
|
||||
const onIssueTrialLicense = useCallback(async () => {
|
||||
// トライアルライセンス発行APIの呼び出し
|
||||
const { meta } = await dispatch(issueTrialLicenseAsync({ selectedRow }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
}
|
||||
}, [dispatch, closePopup, selectedRow]);
|
||||
|
||||
// HTML
|
||||
return (
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("trialLicenseIssuePopupPage.label.title"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("trialLicenseIssuePopupPage.label.subTitle"))}
|
||||
</dt>
|
||||
<dt className={styles.overLine}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.numberOfLicenses"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>{numberOfLicenses}</dd>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.expirationDate"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>{expirationDate}</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.issueButton"
|
||||
)
|
||||
)}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={onIssueTrialLicense}
|
||||
/>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -8,7 +8,12 @@ import Footer from "components/footer";
|
||||
const SupportPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
// OMDS_IS-381 Support画面で表示する内容を充実したいの対応 2024年8月7日
|
||||
const customDivStyles: React.CSSProperties = { padding: "2rem" };
|
||||
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 (
|
||||
@ -26,7 +31,7 @@ const SupportPage: React.FC = () => {
|
||||
<div>
|
||||
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
|
||||
|
||||
<div className={styles.txContents} style={customDivStyles}>
|
||||
<div className={styles.txContents} style={userGuideDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
@ -46,7 +51,7 @@ const SupportPage: React.FC = () => {
|
||||
<h2>
|
||||
{t(getTranslationID("supportPage.label.programDownload"))}
|
||||
</h2>
|
||||
<div className={styles.txContents} style={customDivStyles}>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
@ -71,7 +76,7 @@ const SupportPage: React.FC = () => {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={customDivStyles}>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
@ -92,6 +97,56 @@ const SupportPage: React.FC = () => {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/odms_cloud_backup_extraction_tool/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.backupExtractionToolDownloadLink"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.backupExtractionToolDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/odms_client_virtual_driver/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.virtualDriverDownloadLink"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.virtualDriverDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -28,12 +28,13 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface AllocateLicensePopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -87,6 +88,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [dispatch, closePopup, id, selectedlicenseId, hasErrorEmptyLicense]);
|
||||
@ -219,8 +221,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
value={selectedlicenseId ?? ""}
|
||||
>
|
||||
<option value="" hidden>
|
||||
{`--
|
||||
${t(
|
||||
{`-- ${t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.dropDownHeading"
|
||||
)
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
selectIsLoading,
|
||||
deallocateLicenseAsync,
|
||||
deleteUserAsync,
|
||||
confirmUserForceAsync,
|
||||
} from "features/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
@ -32,6 +33,7 @@ import checkFill from "../../assets/images/check_fill.svg";
|
||||
import checkOutline from "../../assets/images/check_outline.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import upload from "../../assets/images/upload.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import { UserAddPopup } from "./popup";
|
||||
import { UserUpdatePopup } from "./updatePopup";
|
||||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||||
@ -48,6 +50,8 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
||||
useState(false);
|
||||
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
|
||||
const [searchEmail, setSearchEmail] = useState("");
|
||||
const [searchUserName, setSearchUserName] = useState("");
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsPopupOpen(true);
|
||||
@ -84,6 +88,7 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
@ -102,12 +107,53 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const { meta } = await dispatch(deleteUserAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
const onForceEmailVerification = useCallback(
|
||||
async (userId: number) => {
|
||||
// ダイアログ確認
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(
|
||||
t(
|
||||
getTranslationID(
|
||||
"userListPage.message.forceEmailVerificationConfirm"
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(confirmUserForceAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
const requestSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
listUsersAsync({
|
||||
userInputUserName: searchUserName,
|
||||
userInputEmail: searchEmail,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearUserSearchInputs = useCallback(() => {
|
||||
setSearchEmail("");
|
||||
setSearchUserName("");
|
||||
}, [setSearchEmail, setSearchUserName]);
|
||||
|
||||
useEffect(() => {
|
||||
// ユーザ一覧取得処理を呼び出す
|
||||
dispatch(listUsersAsync());
|
||||
@ -126,18 +172,21 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
onClose={() => {
|
||||
setIsUpdatePopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<UserAddPopup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={() => {
|
||||
setIsPopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<AllocateLicensePopup
|
||||
isOpen={isAllocateLicensePopupOpen}
|
||||
onClose={() => {
|
||||
setIsAllocateLicensePopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<ImportPopup
|
||||
isOpen={isImportPopupOpen}
|
||||
@ -185,6 +234,50 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
{t(getTranslationID("userListPage.label.bulkImport"))}
|
||||
</a>
|
||||
</li>
|
||||
<li className={styles.floatRight}>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("userListPage.label.name")
|
||||
)}
|
||||
value={searchUserName}
|
||||
onChange={(e) =>
|
||||
setSearchUserName(e.target.value.trimStart())
|
||||
}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("userListPage.label.email")
|
||||
)}
|
||||
value={searchEmail}
|
||||
onChange={(e) =>
|
||||
setSearchEmail(e.target.value.trimStart())
|
||||
}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
className={`${styles.menuLink} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.search"))}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={`${styles.table} ${styles.user}`}>
|
||||
@ -308,6 +401,23 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
{/* 第五階層の管理者が、メール認証済みではないユーザーの行をマウスオーバーしている場合のみ */}
|
||||
{isTier5 && !user.emailVerified && (
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onForceEmailVerification(user.id);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.label.forceEmailVerification"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
<td> {user.name}</td>
|
||||
|
||||
@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface UserAddPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
|
||||
@ -75,6 +76,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface UserUpdatePopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const closePopup = useCallback(() => {
|
||||
@ -79,6 +80,7 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -454,7 +454,6 @@ h3 + .brCrumb .tlIcon {
|
||||
.brCrumbLicense li a:hover {
|
||||
color: #0084b2;
|
||||
}
|
||||
|
||||
.buttonNormal {
|
||||
display: inline-block;
|
||||
width: 15rem;
|
||||
@ -1699,9 +1698,9 @@ _:-ms-lang(x)::-ms-backdrop,
|
||||
margin-left: 648px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.menuAction {
|
||||
margin-bottom: 0.6rem;
|
||||
position: relative;
|
||||
}
|
||||
.menuAction li {
|
||||
display: inline-block;
|
||||
@ -2059,6 +2058,9 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
height: 34px;
|
||||
position: relative;
|
||||
}
|
||||
.dictation .menuAction:not(:first-child) {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.dictation .menuAction .alignLeft {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -2765,4 +2767,106 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal.isShow .searchModalBox {
|
||||
display: block;
|
||||
}
|
||||
.searchModalBox {
|
||||
display: none;
|
||||
width: 70vw; /* 70% of the viewport width */
|
||||
height: 70vh; /* 70% of the viewport height */
|
||||
max-height: 95vh;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 0.3rem;
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
.searchBar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.searchInput {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: -30px;
|
||||
}
|
||||
.breadcrumbPopup {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #000;
|
||||
padding: 0.3rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
.brCrumbPartner {
|
||||
margin: 0.5rem 0 0.3rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.brCrumbPartner li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04rem;
|
||||
white-space: nowrap;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.brCrumbPartner li:not(:last-child)::after {
|
||||
content: "";
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #333333;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.hoverBlue {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
&:hover {
|
||||
color: #0084b2;
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgb(255, 255, 255);
|
||||
opacity: 0.5;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.overlay .icLoading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
||||
|
||||
@ -233,5 +233,13 @@ declare const classNames: {
|
||||
readonly txContents: "txContents";
|
||||
readonly txIcon: "txIcon";
|
||||
readonly txWswrap: "txWswrap";
|
||||
readonly searchModalBox: "searchModalBox";
|
||||
readonly searchBar: "searchBar";
|
||||
readonly searchInput: "searchInput";
|
||||
readonly headerContainer: "headerContainer";
|
||||
readonly breadcrumbPopup: "breadcrumbPopup";
|
||||
readonly brCrumbPartner: "brCrumbPartner";
|
||||
readonly hoverBlue: "hoverBlue";
|
||||
readonly overlay: "overlay";
|
||||
};
|
||||
export = classNames;
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"text": {
|
||||
"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": {
|
||||
@ -146,7 +146,9 @@
|
||||
"duplicateEmailError": "Die E-Mail-Adressen in den folgenden Zeilen werden in der CSV-Datei dupliziert.",
|
||||
"duplicateAuthorIdError": "Die Autoren-ID in der folgenden Zeile wird in der CSV-Datei dupliziert.",
|
||||
"overMaxUserError": "Durch die Benutzerregistrierung per CSV-Datei können bis zu 100 Benutzer gleichzeitig registriert werden.",
|
||||
"invalidInputError": "Die Benutzerinformationen in der folgenden Zeile entsprechen nicht den Eingaberegeln."
|
||||
"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": {
|
||||
"title": "Benutzer",
|
||||
@ -193,7 +195,9 @@
|
||||
"encryptionLabel": "Verschlüsselung",
|
||||
"encryptionPasswordLabel": "Verschlüsselungspasswort",
|
||||
"promptLabel": "Eingabeaufforderung",
|
||||
"addUsers": "Benutzer hinzufügen"
|
||||
"addUsers": "Benutzer hinzufügen",
|
||||
"forceEmailVerification": "E-Mail-Verifizierung erzwingen",
|
||||
"search": "Suche"
|
||||
},
|
||||
"text": {
|
||||
"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)",
|
||||
"licenseLabel": "Lizenz",
|
||||
"storageLabel": "Lagerung",
|
||||
"storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung"
|
||||
"storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung",
|
||||
"issueTrialLicense": "Testlizenz ausstellen"
|
||||
},
|
||||
"message": {
|
||||
"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.",
|
||||
"backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.",
|
||||
"cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
|
||||
"reopenFailedError": "Der Status kann nicht in „Ausstehend“ geändert werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
|
||||
"deleteFailedError": "Die Aufgabe konnte nicht gelöscht werden. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.",
|
||||
"licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.",
|
||||
"licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.",
|
||||
@ -310,7 +316,9 @@
|
||||
"applications": "Desktopanwendung",
|
||||
"cancelDictation": "Transkription abbrechen",
|
||||
"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": {
|
||||
@ -380,7 +388,9 @@
|
||||
"issueRequesting": "Lizenzen auf Bestellung",
|
||||
"viewDetails": "Details anzeigen",
|
||||
"accounts": "konten",
|
||||
"changeOwnerButton": "Change Owner"
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedLicense": "Zugewiesene Lizenzen",
|
||||
"search": "Suche"
|
||||
}
|
||||
},
|
||||
"orderHistoriesPage": {
|
||||
@ -398,7 +408,8 @@
|
||||
"issue": "Ausgabe",
|
||||
"issueCancel": "Lizenzen kündigen",
|
||||
"orderCancel": "Bestellung stornieren",
|
||||
"histories": "geschichten"
|
||||
"histories": "geschichten",
|
||||
"licenseType": "Lizenztyp"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "Lizenzen konnten aufgrund unzureichender Lizenzanzahl nicht ausgestellt werden. Bitte bestellen Sie zusätzliche Lizenzen.",
|
||||
@ -622,7 +633,11 @@
|
||||
"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."
|
||||
"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": {
|
||||
"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."
|
||||
@ -658,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"lowerLayerId": "Lower Layer ID"
|
||||
}
|
||||
},
|
||||
"trialLicenseIssuePopupPage": {
|
||||
"label": {
|
||||
"title": "Testlizenz ausstellen",
|
||||
"subTitle": "Es werden Testlizenzen ausgestellt.",
|
||||
"numberOfLicenses": "Anzahl der Lizenzen",
|
||||
"expirationDate": "Verfallsdatum",
|
||||
"issueButton": "Ausgabe"
|
||||
},
|
||||
"message": {
|
||||
"accountNotSelected": "Konto ist nicht ausgewählt."
|
||||
}
|
||||
},
|
||||
"searchPartnerAccountPopupPage": {
|
||||
"label": {
|
||||
"title": "Konto durchsuchen"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"text": {
|
||||
"maintenanceNotificationTitle": "Notice of scheduled maintenance",
|
||||
"maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from 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": {
|
||||
@ -146,7 +146,9 @@
|
||||
"duplicateEmailError": "The email addresses in the following lines are duplicated in the CSV file.",
|
||||
"duplicateAuthorIdError": "The Author ID in the following line is duplicated in the CSV file.",
|
||||
"overMaxUserError": "Up to 100 users can be registered at one time by user registration via CSV file.",
|
||||
"invalidInputError": "The user information in the following line does not comply with the input rules."
|
||||
"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": {
|
||||
"title": "User",
|
||||
@ -193,7 +195,9 @@
|
||||
"encryptionLabel": "Encryption",
|
||||
"encryptionPasswordLabel": "Encryption Password",
|
||||
"promptLabel": "Prompt",
|
||||
"addUsers": "Add User"
|
||||
"addUsers": "Add User",
|
||||
"forceEmailVerification": "Force Email Verification",
|
||||
"search": "Search"
|
||||
},
|
||||
"text": {
|
||||
"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)",
|
||||
"licenseLabel": "License",
|
||||
"storageLabel": "Storage",
|
||||
"storageUnavailableCheckbox": "Restrict account usage"
|
||||
"storageUnavailableCheckbox": "Restrict account usage",
|
||||
"issueTrialLicense": "Issue Trial License"
|
||||
},
|
||||
"message": {
|
||||
"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.",
|
||||
"backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.",
|
||||
"cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
|
||||
"reopenFailedError": "The status could not be changed to Pending. Please refresh the screen to see the current status. Only files with Finished status can be operated.",
|
||||
"deleteFailedError": "Failed to delete the task. Please refresh the screen and check again.",
|
||||
"licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.",
|
||||
"licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.",
|
||||
@ -310,7 +316,9 @@
|
||||
"applications": "Desktop Application",
|
||||
"cancelDictation": "Cancel Transcription",
|
||||
"rawFileName": "Original File Name",
|
||||
"fileNameSave": "Execute file rename"
|
||||
"fileNameSave": "Execute file rename",
|
||||
"reopenDictation": "Change status to Pending",
|
||||
"search": "Search"
|
||||
}
|
||||
},
|
||||
"cardLicenseIssuePopupPage": {
|
||||
@ -380,7 +388,9 @@
|
||||
"issueRequesting": "Licenses on Order",
|
||||
"viewDetails": "View Details",
|
||||
"accounts": "accounts",
|
||||
"changeOwnerButton": "Change Owner"
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedLicense": "Allocated Licenses",
|
||||
"search": "Search"
|
||||
}
|
||||
},
|
||||
"orderHistoriesPage": {
|
||||
@ -398,7 +408,8 @@
|
||||
"issue": "Issue",
|
||||
"issueCancel": "Cancel Licenses",
|
||||
"orderCancel": "Cancel Order",
|
||||
"histories": "histories"
|
||||
"histories": "histories",
|
||||
"licenseType": "License Type"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "Licenses could not be issued due to insufficient amount of licenses. Please order additional licenses.",
|
||||
@ -622,7 +633,11 @@
|
||||
"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."
|
||||
"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": {
|
||||
"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."
|
||||
@ -658,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"lowerLayerId": "Lower Layer ID"
|
||||
}
|
||||
},
|
||||
"trialLicenseIssuePopupPage": {
|
||||
"label": {
|
||||
"title": "Issue Trial License",
|
||||
"subTitle": "Trial licenses will be issued.",
|
||||
"numberOfLicenses": "Number of licenses",
|
||||
"expirationDate": "Expiration Date",
|
||||
"issueButton": "Issue"
|
||||
},
|
||||
"message": {
|
||||
"accountNotSelected": "Account is not selected."
|
||||
}
|
||||
},
|
||||
"searchPartnerAccountPopupPage": {
|
||||
"label": {
|
||||
"title": "Search Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"text": {
|
||||
"maintenanceNotificationTitle": "Aviso de mantenimiento programado",
|
||||
"maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 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": {
|
||||
@ -146,7 +146,9 @@
|
||||
"duplicateEmailError": "Las direcciones de correo electrónico de las siguientes líneas están duplicadas en el archivo CSV.",
|
||||
"duplicateAuthorIdError": "El ID del autor en la siguiente línea está duplicado en el archivo CSV.",
|
||||
"overMaxUserError": "Se pueden registrar hasta 100 usuarios a la vez mediante el registro de usuario mediante un archivo CSV.",
|
||||
"invalidInputError": "La información del usuario en la siguiente línea no cumple con las reglas de entrada."
|
||||
"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": {
|
||||
"title": "Usuario",
|
||||
@ -193,7 +195,9 @@
|
||||
"encryptionLabel": "Cifrado ",
|
||||
"encryptionPasswordLabel": "Contraseña de cifrado",
|
||||
"promptLabel": "Solicitar",
|
||||
"addUsers": "Agregar usuario"
|
||||
"addUsers": "Agregar usuario",
|
||||
"forceEmailVerification": "Verificación forzada de correo electrónico",
|
||||
"search": "Búsqueda"
|
||||
},
|
||||
"text": {
|
||||
"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)",
|
||||
"licenseLabel": "Licencia",
|
||||
"storageLabel": "Almacenamiento",
|
||||
"storageUnavailableCheckbox": "Restringir el uso de la cuenta"
|
||||
"storageUnavailableCheckbox": "Restringir el uso de la cuenta",
|
||||
"issueTrialLicense": "Emitir licencia de prueba"
|
||||
},
|
||||
"message": {
|
||||
"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.",
|
||||
"backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.",
|
||||
"cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
|
||||
"reopenFailedError": "No se pudo cambiar el estado a Pendiente. Actualice la pantalla para ver el estado actual. Solo se pueden utilizar los archivos con estado Finalizado.",
|
||||
"deleteFailedError": "No se pudo eliminar la tarea. Actualice la pantalla y verifique nuevamente.",
|
||||
"licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.",
|
||||
"licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.",
|
||||
@ -310,7 +316,9 @@
|
||||
"applications": "Aplicación de escritorio",
|
||||
"cancelDictation": "Cancelar transcripción",
|
||||
"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": {
|
||||
@ -380,7 +388,9 @@
|
||||
"issueRequesting": "Licencias en Pedido",
|
||||
"viewDetails": "Ver detalles",
|
||||
"accounts": "cuentas",
|
||||
"changeOwnerButton": "Change Owner"
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedLicense": "Licencias asignadas",
|
||||
"search": "Búsqueda"
|
||||
}
|
||||
},
|
||||
"orderHistoriesPage": {
|
||||
@ -398,7 +408,8 @@
|
||||
"issue": "Emitida",
|
||||
"issueCancel": "Cancelar licencias",
|
||||
"orderCancel": "Cancelar pedido",
|
||||
"histories": "historias"
|
||||
"histories": "historias",
|
||||
"licenseType": "Tipo de licencia"
|
||||
},
|
||||
"message": {
|
||||
"notEnoughOfNumberOfLicense": "No se pudieron emitir licencias debido a una cantidad insuficiente de licencias. Solicite licencias adicionales.",
|
||||
@ -620,9 +631,13 @@
|
||||
"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 aplicación de escritorio ODMS Cloud.",
|
||||
"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."
|
||||
"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": {
|
||||
"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."
|
||||
@ -658,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"lowerLayerId": "Lower Layer ID"
|
||||
}
|
||||
},
|
||||
"trialLicenseIssuePopupPage": {
|
||||
"label": {
|
||||
"title": "Emitir licencia de prueba",
|
||||
"subTitle": "Se expedirán licencias de prueba.",
|
||||
"numberOfLicenses": "Número de licencias",
|
||||
"expirationDate": "Fecha de caducidad",
|
||||
"issueButton": "Emitida"
|
||||
},
|
||||
"message": {
|
||||
"accountNotSelected": "La cuenta no está seleccionada."
|
||||
}
|
||||
},
|
||||
"searchPartnerAccountPopupPage": {
|
||||
"label": {
|
||||
"title": "Buscar cuenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"text": {
|
||||
"maintenanceNotificationTitle": "Avis de maintenance programmée",
|
||||
"maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 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": {
|
||||
@ -146,7 +146,9 @@
|
||||
"duplicateEmailError": "Les adresses email des lignes suivantes sont dupliquées dans le fichier CSV.",
|
||||
"duplicateAuthorIdError": "L'ID d'auteur dans la ligne suivante est dupliqué dans le fichier CSV.",
|
||||
"overMaxUserError": "Jusqu'à 100 utilisateurs peuvent être enregistrés en même temps par enregistrement d'utilisateur via un fichier CSV.",
|
||||
"invalidInputError": "Les informations utilisateur de la ligne suivante ne sont pas conformes aux règles de saisie."
|
||||
"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": {
|
||||
"title": "Utilisateur",
|
||||
@ -193,7 +195,9 @@
|
||||
"encryptionLabel": "Chiffrement",
|
||||
"encryptionPasswordLabel": "Mot de passe de chiffrement",
|
||||
"promptLabel": "Invite",
|
||||
"addUsers": "Ajouter un utilisateur"
|
||||
"addUsers": "Ajouter un utilisateur",
|
||||
"forceEmailVerification": "Forcer la vérification de l'e-mail",
|
||||
"search": "Recherche"
|
||||
},
|
||||
"text": {
|
||||
"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)",
|
||||
"licenseLabel": "Licence",
|
||||
"storageLabel": "Stockage",
|
||||
"storageUnavailableCheckbox": "Restreindre l'utilisation du compte"
|
||||
"storageUnavailableCheckbox": "Restreindre l'utilisation du compte",
|
||||
"issueTrialLicense": "Délivrer licence d'essai"
|
||||
},
|
||||
"message": {
|
||||
"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.",
|
||||
"backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.",
|
||||
"cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
|
||||
"reopenFailedError": "Le statut n'a pas pu être modifié en Suspendu. Veuillez actualiser l'écran pour voir le statut actuel. Seuls les fichiers dont le statut est Terminé peuvent être traités.",
|
||||
"deleteFailedError": "Échec de la suppression de la tâche. Veuillez actualiser l'écran et vérifier à nouveau.",
|
||||
"licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.",
|
||||
"licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.",
|
||||
@ -310,7 +316,9 @@
|
||||
"applications": "Application de bureau",
|
||||
"cancelDictation": "Annuler la transcription",
|
||||
"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": {
|
||||
@ -380,7 +388,9 @@
|
||||
"issueRequesting": "Licences en commande",
|
||||
"viewDetails": "Voir les détails",
|
||||
"accounts": "comptes",
|
||||
"changeOwnerButton": "Change Owner"
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedLicense": "Licences attribuées",
|
||||
"search": "Recherche"
|
||||
}
|
||||
},
|
||||
"orderHistoriesPage": {
|
||||
@ -388,17 +398,18 @@
|
||||
"title": "Licence",
|
||||
"orderHistory": "Historique des commandes",
|
||||
"orderDate": "Date de commande",
|
||||
"issueDate": "Date de émission",
|
||||
"issueDate": "Date de délivrance",
|
||||
"numberOfOrder": "Nombre de licences commandées",
|
||||
"poNumber": "Numéro de bon de commande",
|
||||
"status": "État",
|
||||
"issueRequesting": "Licences en commande",
|
||||
"issued": "Licence issued",
|
||||
"issued": "Licence délivrée",
|
||||
"orderCanceled": "Commande annulée",
|
||||
"issue": "Problème",
|
||||
"issue": "Délivrer",
|
||||
"issueCancel": "Annuler les licences",
|
||||
"orderCancel": "Annuler commande",
|
||||
"histories": "histoires"
|
||||
"histories": "histoires",
|
||||
"licenseType": "Type de licence"
|
||||
},
|
||||
"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.",
|
||||
@ -620,9 +631,13 @@
|
||||
"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 l'application ODMS Cloud Desktop.",
|
||||
"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."
|
||||
"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": {
|
||||
"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."
|
||||
@ -658,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"lowerLayerId": "Lower Layer ID"
|
||||
}
|
||||
},
|
||||
"trialLicenseIssuePopupPage": {
|
||||
"label": {
|
||||
"title": "Délivrer licence d'essai",
|
||||
"subTitle": "Des licences d’essai 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,9 +39,6 @@ RUN mkdir -p /tmp/gotools \
|
||||
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
|
||||
&& rm -rf /tmp/gotools
|
||||
|
||||
# Update NPM
|
||||
RUN npm install -g npm
|
||||
|
||||
# 以下 ユーザー権限で実施
|
||||
USER $USERNAME
|
||||
# copy init-script
|
||||
|
||||
@ -636,15 +636,15 @@ export async function sendMailWithU108(
|
||||
"utf-8"
|
||||
);
|
||||
html = templateU108NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = templateU108NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
} else {
|
||||
const templateU108Html = readFileSync(
|
||||
path.resolve(__dirname, `../templates/template_U_108.html`),
|
||||
@ -655,17 +655,17 @@ export async function sendMailWithU108(
|
||||
"utf-8"
|
||||
);
|
||||
html = templateU108Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = templateU108Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
}
|
||||
const uniqueCustomerAdminMails = [...new Set(customerAdminMails)];
|
||||
const ccMails = uniqueCustomerAdminMails.includes(userMail) ? [] : [userMail];
|
||||
@ -694,3 +694,10 @@ class autoAllocationList {
|
||||
accountId: number;
|
||||
userIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与えられた文字列内の $ を $$ にエスケープする
|
||||
* @param str - エスケープする対象の文字列
|
||||
* @returns エスケープ後の文字列
|
||||
*/
|
||||
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");
|
||||
@ -37,9 +37,6 @@ RUN mkdir -p /tmp/gotools \
|
||||
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
|
||||
&& rm -rf /tmp/gotools
|
||||
|
||||
# Update NPM
|
||||
RUN npm install -g npm
|
||||
|
||||
# Install NestJS
|
||||
RUN npm i -g @nestjs/cli
|
||||
|
||||
|
||||
@ -2,5 +2,6 @@ DB_HOST=omds-mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=omds
|
||||
DB_NAME_CCB=omds_ccb
|
||||
DB_NAME_PH1ENHANCE=omds_ph1enhance
|
||||
DB_USERNAME=omdsdbuser
|
||||
DB_PASSWORD=omdsdbpass
|
||||
|
||||
@ -6,6 +6,10 @@ ccb:
|
||||
dialect: mysql
|
||||
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
|
||||
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:
|
||||
dialect: mysql
|
||||
dir: ./dictation_server/db/migrations
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `license_orders` ADD COLUMN `type` VARCHAR(255) DEFAULT "NORMAL" COMMENT 'ライセンス種別' AFTER `issued_at`;
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `license_orders` DROP COLUMN `type`;
|
||||
16
dictation_server/db/migrations/068-create_task_filters.sql
Normal file
16
dictation_server/db/migrations/068-create_task_filters.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- +migrate Up
|
||||
CREATE TABLE IF NOT EXISTS `task_filters` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'タスク検索条件ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT 'ユーザーID',
|
||||
`author_id` VARCHAR(255) COMMENT '検索キー:AuthorID',
|
||||
`file_name` VARCHAR(1024) COMMENT '検索キー:ファイル名',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
|
||||
INDEX `idx_task_filters_user_id` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `task_filters`;
|
||||
@ -0,0 +1,9 @@
|
||||
-- +migrate Up
|
||||
INSERT INTO task_filters (user_id)
|
||||
SELECT
|
||||
id AS user_id
|
||||
FROM
|
||||
users;
|
||||
|
||||
-- +migrate Down
|
||||
TRUNCATE TABLE task_filters;
|
||||
@ -27,7 +27,9 @@
|
||||
"migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=local",
|
||||
"migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=local",
|
||||
"migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local",
|
||||
"migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test"
|
||||
"migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test",
|
||||
"migrate:down:test": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=test",
|
||||
"migrate:status:test": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/identity": "^3.1.3",
|
||||
@ -105,6 +107,7 @@
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"testTimeout": 120000,
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
|
||||
@ -1417,6 +1417,119 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/partners/search": {
|
||||
"get": {
|
||||
"operationId": "searchPartners",
|
||||
"summary": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "companyName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "パートナー名",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "accountId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "アカウントID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchPartnersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/partners/hierarchy": {
|
||||
"get": {
|
||||
"operationId": "getPartnerHierarchy",
|
||||
"summary": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "accountId",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"description": "アカウントID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetPartnerHierarchyResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/me/file-delete-setting": {
|
||||
"post": {
|
||||
"operationId": "updateFileDeleteSetting",
|
||||
@ -1968,11 +2081,76 @@
|
||||
"tags": ["users"]
|
||||
}
|
||||
},
|
||||
"/users/confirm/force": {
|
||||
"post": {
|
||||
"operationId": "confirmUserForce",
|
||||
"summary": "",
|
||||
"description": "ユーザーを強制的にメール認証済にする",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ConfirmForceRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ConfirmResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "メール認証済み",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"operationId": "getUsers",
|
||||
"summary": "",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
@ -2184,6 +2362,106 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/task-filters": {
|
||||
"post": {
|
||||
"operationId": "updateTaskFilter",
|
||||
"summary": "",
|
||||
"description": "ログインしているユーザーの検索条件を更新します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostTaskFiltersRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostTaskFiltersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getTaskFilter",
|
||||
"summary": "",
|
||||
"description": "ログインしているユーザーのタスクの検索条件を取得します",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTaskFiltersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/update": {
|
||||
"post": {
|
||||
"operationId": "updateUser",
|
||||
@ -2996,6 +3274,20 @@
|
||||
"in": "query",
|
||||
"description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "authorId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "タスクの検索キーワード:AuthorID",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "fileName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "タスクの検索キーワード:fileName",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -3524,6 +3816,68 @@
|
||||
"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": {
|
||||
"post": {
|
||||
"operationId": "createOrders",
|
||||
@ -3717,6 +4071,62 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/licenses/trial": {
|
||||
"post": {
|
||||
"operationId": "issueTrialLicenses",
|
||||
"summary": "",
|
||||
"description": "第五階層アカウントにトライアルライセンスを発行します。",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IssueTrialLicenseRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IssueTrialLicenseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "アカウントやユーザーが見つからないエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["licenses"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/licenses/orders/cancel": {
|
||||
"post": {
|
||||
"operationId": "cancelOrder",
|
||||
@ -4471,6 +4881,10 @@
|
||||
"type": "number",
|
||||
"description": "不足数({Stock license} - {Issue Requested})"
|
||||
},
|
||||
"allocatedLicense": {
|
||||
"type": "number",
|
||||
"description": "有効期限内の割り当て済み総ライセンス数"
|
||||
},
|
||||
"issueRequesting": {
|
||||
"type": "number",
|
||||
"description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)"
|
||||
@ -4483,6 +4897,7 @@
|
||||
"stockLicense",
|
||||
"issuedRequested",
|
||||
"shortage",
|
||||
"allocatedLicense",
|
||||
"issueRequesting"
|
||||
]
|
||||
},
|
||||
@ -4516,9 +4931,10 @@
|
||||
"issueDate": { "type": "string", "description": "発行日付" },
|
||||
"numberOfOrder": { "type": "number", "description": "注文数" },
|
||||
"poNumber": { "type": "string", "description": "POナンバー" },
|
||||
"status": { "type": "string", "description": "注文状態" }
|
||||
"status": { "type": "string", "description": "注文状態" },
|
||||
"type": { "type": "string", "description": "ライセンス種別" }
|
||||
},
|
||||
"required": ["orderDate", "numberOfOrder", "poNumber", "status"]
|
||||
"required": ["orderDate", "numberOfOrder", "poNumber", "status", "type"]
|
||||
},
|
||||
"GetOrderHistoriesResponse": {
|
||||
"type": "object",
|
||||
@ -4731,6 +5147,60 @@
|
||||
},
|
||||
"required": ["total", "partners"]
|
||||
},
|
||||
"SearchPartner": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "会社名" },
|
||||
"tier": { "type": "number", "description": "階層" },
|
||||
"accountId": { "type": "number", "description": "アカウントID" },
|
||||
"country": { "type": "string", "description": "国" },
|
||||
"primaryAdmin": {
|
||||
"type": "string",
|
||||
"description": "プライマリ管理者"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "プライマリ管理者メールアドレス"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"tier",
|
||||
"accountId",
|
||||
"country",
|
||||
"primaryAdmin",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"SearchPartnersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"searchResult": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/SearchPartner" }
|
||||
}
|
||||
},
|
||||
"required": ["searchResult"]
|
||||
},
|
||||
"PartnerHierarchy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "会社名" },
|
||||
"tier": { "type": "number", "description": "階層" },
|
||||
"accountId": { "type": "number", "description": "アカウントID" }
|
||||
},
|
||||
"required": ["name", "tier", "accountId"]
|
||||
},
|
||||
"GetPartnerHierarchyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accountHierarchy": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/PartnerHierarchy" }
|
||||
}
|
||||
},
|
||||
"required": ["accountHierarchy"]
|
||||
},
|
||||
"UpdateAccountInfoRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4896,6 +5366,11 @@
|
||||
"required": ["token"]
|
||||
},
|
||||
"ConfirmResponse": { "type": "object", "properties": {} },
|
||||
"ConfirmForceRequest": {
|
||||
"type": "object",
|
||||
"properties": { "userId": { "type": "number" } },
|
||||
"required": ["userId"]
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5063,6 +5538,34 @@
|
||||
},
|
||||
"required": ["direction", "paramName"]
|
||||
},
|
||||
"PostTaskFiltersRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filterConditionAuthorId": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを更新する:AuthorID"
|
||||
},
|
||||
"filterConditionFileName": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを更新する:fileName"
|
||||
}
|
||||
},
|
||||
"required": ["filterConditionAuthorId", "filterConditionFileName"]
|
||||
},
|
||||
"PostTaskFiltersResponse": { "type": "object", "properties": {} },
|
||||
"GetTaskFiltersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorId": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを取得する:AuthorID"
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを取得する:fileName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PostUpdateUserRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5553,6 +6056,12 @@
|
||||
},
|
||||
"required": ["allocatableLicenses"]
|
||||
},
|
||||
"IssueTrialLicenseRequest": {
|
||||
"type": "object",
|
||||
"properties": { "issuedAccount": { "type": "number" } },
|
||||
"required": ["issuedAccount"]
|
||||
},
|
||||
"IssueTrialLicenseResponse": { "type": "object", "properties": {} },
|
||||
"CancelOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": { "poNumber": { "type": "string" } },
|
||||
|
||||
@ -41,6 +41,7 @@ import { LicensesController } from './features/licenses/licenses.controller';
|
||||
import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module';
|
||||
import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module';
|
||||
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from './repositories/task_filters/task_filter.repository.module';
|
||||
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
|
||||
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
|
||||
import { TemplatesService } from './features/templates/templates.service';
|
||||
@ -138,6 +139,7 @@ import { JobNumberRepositoryModule } from './repositories/job_number/job_number.
|
||||
AuthGuardsModule,
|
||||
SystemAccessGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
TermsModule,
|
||||
RedisModule,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type NotificationBody = {
|
||||
id: string;
|
||||
filename: string;
|
||||
authorId: string;
|
||||
priority: string;
|
||||
|
||||
8
dictation_server/src/common/repository/utils/utils.ts
Normal file
8
dictation_server/src/common/repository/utils/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* LIKE句で使用する文字列のエスケープ
|
||||
* @param value エスケープする文字列
|
||||
* @returns エスケープ後の文字列
|
||||
*/
|
||||
export function escapeLikeString(value: string): string {
|
||||
return value.replace(/[%_]/g, (match) => `\\${match}`);
|
||||
}
|
||||
@ -23,6 +23,7 @@ import { NotificationhubModule } from '../../gateways/notificationhub/notificati
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
import { AuthGuardsModule } from '../../common/guards/auth/authguards.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../repositories/task_filters/task_filter.repository.module';
|
||||
import { AuthService } from '../../features/auth/auth.service';
|
||||
import { AccountsService } from '../../features/accounts/accounts.service';
|
||||
import { UsersService } from '../../features/users/users.service';
|
||||
@ -78,6 +79,7 @@ export const makeTestingModule = async (
|
||||
BlobstorageModule,
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
TermsRepositoryModule,
|
||||
JobNumberRepositoryModule,
|
||||
|
||||
@ -10,6 +10,7 @@ import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.servi
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
import { SearchPartnerInfoFromDb } from '../../features/accounts/types/types';
|
||||
|
||||
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
|
||||
|
||||
@ -279,6 +280,14 @@ export const overrideAccountsRepositoryService = <TService>(
|
||||
) => Promise<{ newAccount: Account; adminUser: User }>;
|
||||
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
|
||||
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>;
|
||||
getAccountsRelatedOwnAccount?: (
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownAccountTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
) => Promise<SearchPartnerInfoFromDb[]>;
|
||||
findUserByExternalId?: (context: Context, sub: string) => Promise<User>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
|
||||
@ -12,6 +12,7 @@ import { AccountArchive } from '../../repositories/accounts/entity/account_archi
|
||||
import { Task } from '../../repositories/tasks/entity/task.entity';
|
||||
import { JobNumber } from '../../repositories/job_number/entity/job_number.entity';
|
||||
import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../../repositories/task_filters/entity/task_filters.entity';
|
||||
|
||||
type InitialTestDBState = {
|
||||
tier1Accounts: { account: Account; users: User[] }[];
|
||||
@ -243,6 +244,9 @@ export const makeTestAccount = async (
|
||||
// sort_criteriaテーブルにデータを追加
|
||||
await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
// task_filtersテーブルにデータを追加
|
||||
await createTaskFilter(datasource, userId, null, null);
|
||||
|
||||
// job_numberテーブルにデータを追加
|
||||
await createJobNumber(datasource, accountId, '00000000');
|
||||
|
||||
@ -333,6 +337,10 @@ export const makeTestUser = async (
|
||||
}
|
||||
// sort_criteriaテーブルにデータを追加
|
||||
await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC');
|
||||
|
||||
// task_filtersテーブルにデータを追加
|
||||
await createTaskFilter(datasource, user.id, null, null);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
@ -523,3 +531,46 @@ export const getSortCriteria = async (
|
||||
});
|
||||
return sortCriteria;
|
||||
};
|
||||
|
||||
// task_filterを作成する
|
||||
export const createTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(TaskFilters).insert({
|
||||
user_id: userId,
|
||||
author_id: authorId,
|
||||
file_name: fileName,
|
||||
});
|
||||
};
|
||||
|
||||
// 指定したユーザーのtask_filterを更新する
|
||||
export const updateTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(TaskFilters).update(
|
||||
{ user_id: userId },
|
||||
{
|
||||
author_id: authorId,
|
||||
file_name: fileName,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 指定したユーザーのtask_filterを取得する
|
||||
export const getTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
): Promise<TaskFilters | null> => {
|
||||
const taskFilter = await datasource.getRepository(TaskFilters).findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
return taskFilter;
|
||||
};
|
||||
|
||||
@ -28,6 +28,10 @@ export class EnvValidator {
|
||||
@IsString()
|
||||
DB_NAME_CCB: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
DB_NAME_PH1ENHANCE: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
DB_USERNAME: string;
|
||||
|
||||
@ -203,13 +203,6 @@ export const TASK_LIST_SORTABLE_ATTRIBUTES = [
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
export const MAX_JOB_NUMBER = '99999999';
|
||||
|
||||
/**
|
||||
* 上位階層によるトライアルライセンス発行数
|
||||
* @const {number}
|
||||
*/
|
||||
export const ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY = 10;
|
||||
|
||||
@ -85,6 +85,10 @@ import {
|
||||
GetPartnerUsersRequest,
|
||||
UpdatePartnerInfoRequest,
|
||||
UpdatePartnerInfoResponse,
|
||||
SearchPartnersResponse,
|
||||
SearchPartnersRequest,
|
||||
GetPartnerHierarchyRequest,
|
||||
GetPartnerHierarchyResponse,
|
||||
} from './types/types';
|
||||
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
@ -1885,6 +1889,187 @@ export class AccountsController {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('/partners/search')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: SearchPartnersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'searchPartners' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4],
|
||||
}),
|
||||
)
|
||||
async searchPartners(
|
||||
@Req() req: Request,
|
||||
@Query() query: SearchPartnersRequest,
|
||||
): Promise<SearchPartnersResponse> {
|
||||
// アカウント名の前方スペースを削除
|
||||
const companyName = query.companyName?.trimStart();
|
||||
const accountId = query.accountId;
|
||||
|
||||
// 両方とも未入力の場合はエラー
|
||||
if (!companyName && !accountId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// アカウント名が2文字以下の場合はエラー
|
||||
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
|
||||
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
|
||||
if (companyName && [...companyName].length <= 2) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, tier } = decodedAccessToken as AccessToken;
|
||||
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
const response = await this.accountService.searchPartners(
|
||||
context,
|
||||
userId,
|
||||
tier,
|
||||
companyName,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('/partners/hierarchy')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetPartnerHierarchyResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'getPartnerHierarchy' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4],
|
||||
}),
|
||||
)
|
||||
async getPartnerHierarchy(
|
||||
@Req() req: Request,
|
||||
@Query() query: GetPartnerHierarchyRequest,
|
||||
): Promise<GetPartnerHierarchyResponse> {
|
||||
const { accountId } = query;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, tier } = decodedAccessToken as AccessToken;
|
||||
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
const response = await this.accountService.getPartnerHierarchy(
|
||||
context,
|
||||
userId,
|
||||
tier,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('/me')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -38,6 +38,10 @@ import {
|
||||
Partner,
|
||||
GetCompanyNameResponse,
|
||||
PartnerUser,
|
||||
SearchPartnersResponse,
|
||||
GetPartnerHierarchyResponse,
|
||||
SearchPartner,
|
||||
PartnerHierarchy,
|
||||
} from './types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
@ -981,13 +985,18 @@ export class AccountsService {
|
||||
// 各子アカウントのShortageを算出してreturn用の変数にマージする
|
||||
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
|
||||
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
|
||||
const { allocatableLicenseWithMargin, expiringSoonLicense } =
|
||||
childPartnerLicenseFromRepository;
|
||||
const {
|
||||
allocatableLicenseWithMargin,
|
||||
expiringSoonLicense,
|
||||
allocatedLicense,
|
||||
} = childPartnerLicenseFromRepository;
|
||||
|
||||
let childShortage = 0;
|
||||
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
|
||||
if (
|
||||
allocatableLicenseWithMargin === undefined ||
|
||||
expiringSoonLicense === undefined
|
||||
expiringSoonLicense === undefined ||
|
||||
allocatedLicense === undefined
|
||||
) {
|
||||
throw new Error(
|
||||
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
|
||||
@ -1008,6 +1017,9 @@ export class AccountsService {
|
||||
{
|
||||
shortage: childShortage,
|
||||
},
|
||||
{
|
||||
allocatedLicense: allocatedLicense,
|
||||
},
|
||||
);
|
||||
|
||||
childrenPartnerLicenses.push(childPartnerLicense);
|
||||
@ -1072,6 +1084,7 @@ export class AccountsService {
|
||||
orderDate: new Date(licenseOrder.ordered_at).toISOString(),
|
||||
poNumber: licenseOrder.po_number,
|
||||
status: licenseOrder.status,
|
||||
type: licenseOrder.type,
|
||||
};
|
||||
orderHistories.push(returnLicenseOrder);
|
||||
}
|
||||
@ -2159,6 +2172,166 @@ export class AccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナー一覧を検索します。
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param ownTier
|
||||
* @param companyName
|
||||
* @param targetAccountId
|
||||
* @returns SearchPartnersResponse
|
||||
*/
|
||||
async searchPartners(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
ownTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
): Promise<SearchPartnersResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.searchPartners.name
|
||||
} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`ownTier: ${ownTier}, ` +
|
||||
`companyName: ${companyName}, ` +
|
||||
`targetAccountId: ${targetAccountId}, };`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
|
||||
const partnersRecords =
|
||||
await this.accountRepository.getAccountsRelatedOwnAccount(
|
||||
context,
|
||||
accountId,
|
||||
ownTier,
|
||||
companyName,
|
||||
targetAccountId,
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
let externalIds = partnersRecords.map((x) => x.primaryAccountExternalId);
|
||||
externalIds = externalIds.filter((item) => item !== undefined);
|
||||
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
|
||||
|
||||
// DBから取得した情報とADB2Cから取得した情報をマージ
|
||||
const searchResult = partnersRecords.map(
|
||||
(dbuser): SearchPartner => {
|
||||
const adb2cUser = adb2cUsers.find(
|
||||
(adb2c) => dbuser.primaryAccountExternalId === adb2c.id,
|
||||
);
|
||||
if (!adb2cUser) {
|
||||
throw new Error(
|
||||
`adb2c user not found. externalId: ${dbuser.primaryAccountExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { displayName: primaryAdmin, emailAddress: mail } =
|
||||
getUserNameAndMailAddress(adb2cUser);
|
||||
if (!mail) {
|
||||
throw new Error(
|
||||
`adb2c user mail not found. externalId: ${dbuser.primaryAccountExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: dbuser.name,
|
||||
tier: dbuser.tier,
|
||||
accountId: dbuser.accountId,
|
||||
country: dbuser.country,
|
||||
primaryAdmin: primaryAdmin,
|
||||
email: mail,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return { searchResult };
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.searchPartners.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナーの階層構造を取得します
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param ownTier
|
||||
* @param targetAccountId
|
||||
* @returns GetPartnersResponse
|
||||
*/
|
||||
async getPartnerHierarchy(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
ownTier: number,
|
||||
targetAccountId: number,
|
||||
): Promise<GetPartnerHierarchyResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.getPartnerHierarchy.name
|
||||
} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`ownTier: ${ownTier}, ` +
|
||||
`targetAccountId: ${targetAccountId}, };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 自身のアカウントIdを取得
|
||||
const { account_id: ownAccountId } =
|
||||
await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
|
||||
// 対象の親アカウントを取得
|
||||
const parentAccountIds = await this.accountRepository.getHierarchyParents(
|
||||
context,
|
||||
targetAccountId,
|
||||
);
|
||||
|
||||
// 親アカウントの中に自身が存在しない場合、エラー
|
||||
if (!parentAccountIds.includes(ownAccountId)) {
|
||||
throw new Error(
|
||||
`parent account not found. targetAccountId=${targetAccountId}, parentAccountIds=${parentAccountIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 対象を含むアカウント階層をすべて取得
|
||||
const targetAccountIds = [...parentAccountIds, targetAccountId];
|
||||
const accounts = await this.accountRepository.findAccountsById(
|
||||
context,
|
||||
targetAccountIds,
|
||||
);
|
||||
const accountHierarchy = accounts
|
||||
// 取得する階層を自身の階層までに限定
|
||||
.filter((account) => account.tier >= ownTier)
|
||||
.map((account): PartnerHierarchy => {
|
||||
const { tier, id: accountId, company_name: name } = account;
|
||||
return { tier, accountId, name };
|
||||
})
|
||||
// 上位の階層順になるようにソート
|
||||
.sort((a, b) => a.tier - b.tier);
|
||||
|
||||
return { accountHierarchy };
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.getPartnerHierarchy.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウント情報を設定する
|
||||
* @param context
|
||||
|
||||
@ -463,6 +463,7 @@ export const makeDefaultLicensesRepositoryMockValue =
|
||||
numberOfOrder: 10,
|
||||
poNumber: 'PO001',
|
||||
status: 'Issued',
|
||||
type: 'NORMAL',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
LicenseOrder,
|
||||
} from '../../../repositories/licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../..//../repositories/task_filters/entity/task_filters.entity';
|
||||
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
|
||||
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
|
||||
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
|
||||
@ -21,6 +22,15 @@ export const getSortCriteriaList = async (dataSource: DataSource) => {
|
||||
return await dataSource.getRepository(SortCriteria).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのTask Filtersを取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当ユーザー一覧
|
||||
*/
|
||||
export const getTaskFilterList = async (dataSource: DataSource) => {
|
||||
return await dataSource.getRepository(TaskFilters).find();
|
||||
};
|
||||
|
||||
export const createLicense = async (
|
||||
datasource: DataSource,
|
||||
licenseId: number,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, OmitType, PickType } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsInt,
|
||||
@ -320,6 +320,21 @@ export class GetPartnersRequest {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export class SearchPartnersRequest {
|
||||
@ApiProperty({ description: 'パートナー名', required: false })
|
||||
@Type(() => String)
|
||||
companyName: string;
|
||||
@ApiProperty({ description: 'アカウントID', required: false })
|
||||
@Type(() => Number)
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export class GetPartnerHierarchyRequest {
|
||||
@ApiProperty({ description: 'アカウントID' })
|
||||
@Type(() => Number)
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export class UpdateAccountInfoRequest {
|
||||
@ApiProperty({ description: '親アカウントのID', required: false })
|
||||
@Type(() => Number)
|
||||
@ -592,6 +607,9 @@ export class PartnerLicenseInfo {
|
||||
@ApiProperty({ description: '不足数({Stock license} - {Issue Requested})' })
|
||||
shortage: number;
|
||||
|
||||
@ApiProperty({ description: '有効期限内の割り当て済み総ライセンス数' })
|
||||
allocatedLicense?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)',
|
||||
@ -618,6 +636,8 @@ export class LicenseOrder {
|
||||
poNumber: string;
|
||||
@ApiProperty({ description: '注文状態' })
|
||||
status: string;
|
||||
@ApiProperty({ description: 'ライセンス種別' })
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class GetOrderHistoriesResponse {
|
||||
@ -706,6 +726,13 @@ export class Partner {
|
||||
dealerManagement: boolean;
|
||||
}
|
||||
|
||||
export class SearchPartner extends OmitType(Partner, ['dealerManagement']) {}
|
||||
export class PartnerHierarchy extends PickType(Partner, [
|
||||
'tier',
|
||||
'name',
|
||||
'accountId',
|
||||
]) {}
|
||||
|
||||
export class GetPartnersResponse {
|
||||
@ApiProperty({ description: '合計件数' })
|
||||
total: number;
|
||||
@ -713,6 +740,16 @@ export class GetPartnersResponse {
|
||||
partners: Partner[];
|
||||
}
|
||||
|
||||
export class SearchPartnersResponse {
|
||||
@ApiProperty({ type: [SearchPartner] })
|
||||
searchResult: SearchPartner[];
|
||||
}
|
||||
|
||||
export class GetPartnerHierarchyResponse {
|
||||
@ApiProperty({ type: [PartnerHierarchy] })
|
||||
accountHierarchy: PartnerHierarchy[];
|
||||
}
|
||||
|
||||
export class UpdateAccountInfoResponse {}
|
||||
|
||||
export class DeleteAccountResponse {}
|
||||
@ -812,3 +849,10 @@ export type PartnerInfoFromDb = {
|
||||
primaryAccountExternalId: string;
|
||||
dealerManagement: boolean;
|
||||
};
|
||||
|
||||
// パートナー検索にて、RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
|
||||
// dealerManagementを除外
|
||||
export type SearchPartnerInfoFromDb = Omit<
|
||||
PartnerInfoFromDb,
|
||||
'dealerManagement'
|
||||
>;
|
||||
|
||||
@ -367,10 +367,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -472,10 +473,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -737,10 +739,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -1408,10 +1411,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -1526,10 +1530,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -1654,10 +1659,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -1782,10 +1788,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -1900,10 +1907,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -2050,10 +2058,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -2199,10 +2208,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
@ -2349,10 +2359,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
|
||||
@ -273,12 +273,17 @@ export class FilesService {
|
||||
this.logger.log(`[${context.getTrackingId()}] tags: ${tags}`);
|
||||
|
||||
// タグ対象に通知送信
|
||||
await this.notificationhubService.notify(context, tags, {
|
||||
authorId: authorId,
|
||||
filename: fileName.replace('.zip', ''),
|
||||
priority: priority === '00' ? 'Normal' : 'High',
|
||||
uploadedAt: uploadedDate,
|
||||
});
|
||||
await Promise.all(
|
||||
tags.map((tag) => {
|
||||
return this.notificationhubService.notify(context, tag, {
|
||||
id: tag.split('user_')[1],
|
||||
authorId: authorId,
|
||||
filename: fileName.replace('.zip', ''),
|
||||
priority: priority === '00' ? 'Normal' : 'High',
|
||||
uploadedAt: uploadedDate,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// 追加したタスクのJOBナンバーを返却
|
||||
return { jobNumber: task.job_number };
|
||||
|
||||
@ -27,6 +27,8 @@ import {
|
||||
GetAllocatableLicensesResponse,
|
||||
CancelOrderRequest,
|
||||
CancelOrderResponse,
|
||||
IssueTrialLicenseResponse,
|
||||
IssueTrialLicenseRequest,
|
||||
} from './types/types';
|
||||
import { Request } from 'express';
|
||||
import { retrieveAuthorizationToken } from '../../common/http/helper';
|
||||
@ -347,6 +349,90 @@ export class LicensesController {
|
||||
return allocatableLicenses;
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: IssueTrialLicenseResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'アカウントやユーザーが見つからないエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'issueTrialLicenses',
|
||||
description: '第五階層アカウントにトライアルライセンスを発行します。',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/trial')
|
||||
async issueTrialLicense(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@Req() req: Request,
|
||||
@Body() body: IssueTrialLicenseRequest,
|
||||
): Promise<IssueTrialLicenseResponse> {
|
||||
const { issuedAccount } = body;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
await this.licensesService.issueTrialLicense(
|
||||
context,
|
||||
userId,
|
||||
issuedAccount,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: CancelOrderResponse,
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { NewAllocatedLicenseExpirationDate } from './types/types';
|
||||
import {
|
||||
NewAllocatedLicenseExpirationDate,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from './types/types';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { LicensesService } from './licenses.service';
|
||||
@ -15,12 +18,14 @@ import {
|
||||
selectLicenseAllocationHistory,
|
||||
createOrder,
|
||||
selectOrderLicense,
|
||||
selectIssuedLicensesAndLicenseOrders,
|
||||
} from './test/utility';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { Context, makeContext } from '../../common/log';
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
LICENSE_TYPE,
|
||||
} from '../../constants';
|
||||
import {
|
||||
@ -36,6 +41,7 @@ import {
|
||||
} from '../../common/test/overrides';
|
||||
import { truncateAllTable } from '../../common/test/init';
|
||||
import { TestLogger } from '../../common/test/logger';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
|
||||
describe('ライセンス注文', () => {
|
||||
let source: DataSource | null = null;
|
||||
@ -103,6 +109,8 @@ describe('ライセンス注文', () => {
|
||||
expect(dbSelectResult.orderLicense?.from_account_id).toEqual(accountId);
|
||||
expect(dbSelectResult.orderLicense?.to_account_id).toEqual(parentAccountId);
|
||||
expect(dbSelectResult.orderLicense?.status).toEqual('Issue Requesting');
|
||||
// ライセンス種別のデフォルト値が埋まっていること
|
||||
expect(dbSelectResult.orderLicense?.type).toEqual(LICENSE_TYPE.NORMAL);
|
||||
});
|
||||
|
||||
it('POナンバー重複時、エラーとなる', async () => {
|
||||
@ -736,7 +744,7 @@ describe('ライセンス割り当て', () => {
|
||||
);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideAdB2cService(service, {
|
||||
getUser: async (context, externalId) => {
|
||||
@ -2009,3 +2017,385 @@ describe('割り当て可能なライセンス取得', () => {
|
||||
expect(response.allocatableLicenses[5].licenseId).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('第五階層へのトライアルライセンス発行', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeAll(async () => {
|
||||
if (source == null) {
|
||||
source = await (async () => {
|
||||
const s = new DataSource({
|
||||
type: 'mysql',
|
||||
host: 'test_mysql_db',
|
||||
port: 3306,
|
||||
username: 'user',
|
||||
password: 'password',
|
||||
database: 'odms',
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
logger: new TestLogger('none'),
|
||||
logging: true,
|
||||
});
|
||||
return await s.initialize();
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
if (source) {
|
||||
await truncateAllTable(source);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await source?.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('第一階層が第五階層へのトライアルライセンス発行が完了する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
// アカウントの階層構造を作成。
|
||||
const { tier1Accounts, tier4Accounts } = await makeHierarchicalAccounts(
|
||||
source,
|
||||
);
|
||||
const tier1AccountId = tier1Accounts[0].account.id;
|
||||
const tier1ExternalId = tier1Accounts[0].users[0].external_id;
|
||||
const tier4AccountId = tier4Accounts[0].account.id;
|
||||
const { id: tier5AccountId } = await makeTestSimpleAccount(source, {
|
||||
tier: 5,
|
||||
parent_account_id: tier4AccountId,
|
||||
});
|
||||
|
||||
await makeTestUser(source, {
|
||||
account_id: tier5AccountId,
|
||||
external_id: 'tier5UserId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const usersService = module.get<UsersService>(UsersService);
|
||||
const licenseService = module.get<LicensesService>(LicensesService);
|
||||
const sendGridService = module.get<SendGridService>(SendGridService);
|
||||
|
||||
// メール送信サービスのモックによって初期化される値たち
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
let addressToTier5Admin = '';
|
||||
let addressCcDealerList: string[] = [];
|
||||
|
||||
// ユーザー取得処理をモック化
|
||||
overrideAdB2cService(usersService, {
|
||||
getUsers: async (context, externalIds) => {
|
||||
if (externalIds.includes('tier5UserId')) {
|
||||
// 第五階層のユーザーの場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: `tier5Admin${x}`,
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `tier5Admin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 第五階層以外の場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: 'upperTieradmin',
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `upperTierAdmin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// メール送信サービスをモック化
|
||||
overrideSendgridService(licenseService, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const urls = text.match(urlPattern);
|
||||
const url = urls?.pop();
|
||||
|
||||
// 件名
|
||||
_subject = subject;
|
||||
// URL
|
||||
_url = url;
|
||||
// 第5階層の宛先
|
||||
addressToTier5Admin = to[0];
|
||||
// ディーラーの宛先
|
||||
addressCcDealerList = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
await licenseService.issueTrialLicense(
|
||||
context,
|
||||
tier1ExternalId,
|
||||
tier5AccountId,
|
||||
);
|
||||
const dbSelectResult = await selectIssuedLicensesAndLicenseOrders(
|
||||
source,
|
||||
tier5AccountId,
|
||||
tier1AccountId,
|
||||
);
|
||||
if (dbSelectResult === null) fail();
|
||||
|
||||
const { order, licenses } = dbSelectResult;
|
||||
|
||||
// 注文の確認
|
||||
expect(order.from_account_id).toEqual(tier5AccountId);
|
||||
expect(order.to_account_id).toEqual(tier1AccountId);
|
||||
expect(order.po_number).toBeNull();
|
||||
expect(order.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(order.status).toEqual(LICENSE_ISSUE_STATUS.ISSUED);
|
||||
// 10個注文されている
|
||||
expect(order.quantity).toEqual(10);
|
||||
|
||||
// ライセンスの確認
|
||||
// 値のチェックは最初と最後の1つのみ
|
||||
// 10個発行されている
|
||||
expect(licenses.length).toEqual(10);
|
||||
|
||||
// 1レコード目
|
||||
const firstLicense = licenses[0];
|
||||
expect(firstLicense.account_id).toEqual(tier5AccountId);
|
||||
expect(firstLicense.order_id).toEqual(order.id);
|
||||
expect(firstLicense.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(firstLicense.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(firstLicense.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// 10レコード目
|
||||
const lastLicense = licenses.slice(-1)[0];
|
||||
expect(lastLicense?.account_id).toEqual(tier5AccountId);
|
||||
expect(lastLicense?.order_id).toEqual(order.id);
|
||||
expect(lastLicense?.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(lastLicense?.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(lastLicense?.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// メールが期待通り送信されていること
|
||||
// 件名
|
||||
expect(_subject).toBe('Issued Trial License Notification [U-125]');
|
||||
// URL
|
||||
expect(_url).toBe('http://localhost:8081/');
|
||||
// 第五階層の宛先
|
||||
expect(addressToTier5Admin).toBeTruthy();
|
||||
// ディーラーの宛先(第一階層宛のみ)
|
||||
expect(addressCcDealerList).toHaveLength(1);
|
||||
// メール送信が呼ばれた回数を検査(第五階層と第一階層に同じメールを送信)
|
||||
expect(sendGridService.sendMail).toBeCalledTimes(1);
|
||||
});
|
||||
it('第二階層が第五階層へのトライアルライセンス発行が完了する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
// アカウントの階層構造を作成。
|
||||
const { tier2Accounts, tier4Accounts } = await makeHierarchicalAccounts(
|
||||
source,
|
||||
);
|
||||
const tier2AccountId = tier2Accounts[0].account.id;
|
||||
const tier2ExternalId = tier2Accounts[0].users[0].external_id;
|
||||
const tier4AccountId = tier4Accounts[0].account.id;
|
||||
const { id: tier5AccountId } = await makeTestSimpleAccount(source, {
|
||||
tier: 5,
|
||||
parent_account_id: tier4AccountId,
|
||||
});
|
||||
|
||||
await makeTestUser(source, {
|
||||
account_id: tier5AccountId,
|
||||
external_id: 'tier5UserId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const usersService = module.get<UsersService>(UsersService);
|
||||
const licenseService = module.get<LicensesService>(LicensesService);
|
||||
const sendGridService = module.get<SendGridService>(SendGridService);
|
||||
|
||||
// メール送信サービスのモックによって初期化される値たち
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
let expirationDate: string | undefined = '';
|
||||
let addressToTier5Admin = '';
|
||||
let addressCcDealerList: string[] = [];
|
||||
|
||||
// ユーザー取得処理をモック化
|
||||
overrideAdB2cService(usersService, {
|
||||
getUsers: async (context, externalIds) => {
|
||||
if (externalIds.includes('tier5UserId')) {
|
||||
// 第五階層のユーザーの場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: `tier5Admin${x}`,
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `tier5Admin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 第五階層以外の場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: 'upperTieradmin',
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `upperTierAdmin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// メール送信サービスをモック化
|
||||
overrideSendgridService(licenseService, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const urls = text.match(urlPattern);
|
||||
const url = urls?.pop();
|
||||
|
||||
// 件名
|
||||
_subject = subject;
|
||||
// URL
|
||||
_url = url;
|
||||
// 第5階層の宛先
|
||||
addressToTier5Admin = to[0];
|
||||
// ディーラーの宛先
|
||||
addressCcDealerList = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
await licenseService.issueTrialLicense(
|
||||
context,
|
||||
tier2ExternalId,
|
||||
tier5AccountId,
|
||||
);
|
||||
const dbSelectResult = await selectIssuedLicensesAndLicenseOrders(
|
||||
source,
|
||||
tier5AccountId,
|
||||
tier2AccountId,
|
||||
);
|
||||
if (dbSelectResult === null) fail();
|
||||
|
||||
const { order, licenses } = dbSelectResult;
|
||||
|
||||
// 注文の確認
|
||||
expect(order.from_account_id).toEqual(tier5AccountId);
|
||||
expect(order.to_account_id).toEqual(tier2AccountId);
|
||||
expect(order.po_number).toBeNull();
|
||||
expect(order.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(order.status).toEqual(LICENSE_ISSUE_STATUS.ISSUED);
|
||||
// 10個注文されている
|
||||
expect(order.quantity).toEqual(10);
|
||||
|
||||
// ライセンスの確認
|
||||
// 値のチェックは最初と最後の1つのみ
|
||||
// 10個発行されている
|
||||
expect(licenses.length).toEqual(10);
|
||||
|
||||
// 1レコード目
|
||||
const firstLicense = licenses[0];
|
||||
expect(firstLicense.account_id).toEqual(tier5AccountId);
|
||||
expect(firstLicense.order_id).toEqual(order.id);
|
||||
expect(firstLicense.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(firstLicense.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(firstLicense.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// 10レコード目
|
||||
const lastLicense = licenses.slice(-1)[0];
|
||||
expect(lastLicense?.account_id).toEqual(tier5AccountId);
|
||||
expect(lastLicense?.order_id).toEqual(order.id);
|
||||
expect(lastLicense?.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(lastLicense?.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(lastLicense?.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// メールが期待通り送信されていること
|
||||
// 件名
|
||||
expect(_subject).toBe('Issued Trial License Notification [U-125]');
|
||||
// URL
|
||||
expect(_url).toBe('http://localhost:8081/');
|
||||
// 第五階層の宛先
|
||||
expect(addressToTier5Admin).toBeTruthy();
|
||||
// ディーラーの宛先(第一階層宛と第二階層宛)
|
||||
expect(addressCcDealerList).toHaveLength(2);
|
||||
// メール送信が呼ばれた回数を検査(第五階層と第一階層に同じメールを送信)
|
||||
expect(sendGridService.sendMail).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { external_id: externalId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'userId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const service = module.get<LicensesService>(LicensesService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
//DBアクセスに失敗するようにする
|
||||
const licensesService = module.get<LicensesRepositoryService>(
|
||||
LicensesRepositoryService,
|
||||
);
|
||||
licensesService.issueTrialLicense = jest
|
||||
.fn()
|
||||
.mockRejectedValue('DB failed');
|
||||
|
||||
try {
|
||||
await service.issueTrialLicense(context, externalId, accountId);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,12 +14,19 @@ import { UserNotFoundError } from '../../repositories/users/errors/types';
|
||||
import {
|
||||
GetAllocatableLicensesResponse,
|
||||
IssueCardLicensesResponse,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from './types/types';
|
||||
import { Context } from '../../common/log';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
|
||||
import { LICENSE_ISSUE_STATUS } from '../../constants';
|
||||
import {
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
TIERS,
|
||||
TRIAL_LICENSE_EXPIRATION_DAYS,
|
||||
} from '../../constants';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesService {
|
||||
@ -452,6 +459,106 @@ export class LicensesService {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* トライアルライセンスを発行する。発行したライセンスに紐づくライセンス注文も作成する。
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param issuedAccountId
|
||||
*/
|
||||
async issueTrialLicense(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
issuedAccountId: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.issueTrialLicense.name
|
||||
} | params: { externalId: ${externalId}, issuedAccountId: ${issuedAccountId} };`,
|
||||
);
|
||||
|
||||
let me: User;
|
||||
let ownAccountId: number;
|
||||
|
||||
// ユーザIDからアカウントIDを取得する
|
||||
try {
|
||||
me = await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
ownAccountId = me.account_id;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
switch (e.constructor) {
|
||||
case UserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// トライアルライセンスを発行
|
||||
const nowDate = new Date();
|
||||
const expired = new NewTrialLicenseExpirationDate(nowDate);
|
||||
try {
|
||||
await this.licensesRepository.issueTrialLicense(
|
||||
context,
|
||||
issuedAccountId,
|
||||
ownAccountId,
|
||||
nowDate,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] issue traial lisences failed`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// 第五階層へメール送信
|
||||
// 第五階層アカウント名と管理者メールアドレスを取得して送信
|
||||
const {
|
||||
adminEmails: tier5AdminMaileAddresses,
|
||||
companyName: tier5ComponyName,
|
||||
} = await this.getAccountInformation(context, issuedAccountId);
|
||||
|
||||
// 自アカウントの管理者にもメール通知
|
||||
const dealerEmails = (
|
||||
await this.getAccountInformation(context, ownAccountId)
|
||||
).adminEmails;
|
||||
|
||||
// 第二階層によるトライアルライセンス発行の場合、第一階層の管理者にも通知する。
|
||||
if (me.account?.tier === TIERS.TIER2 && me.account?.parent_account_id) {
|
||||
const tire1AdminMails = (await this.getAccountInformation(
|
||||
context,
|
||||
me.account.parent_account_id,
|
||||
)).adminEmails;
|
||||
dealerEmails.unshift(...tire1AdminMails);
|
||||
}
|
||||
await this.sendgridService.sendMailWithU125(
|
||||
context,
|
||||
tier5AdminMaileAddresses,
|
||||
tier5ComponyName,
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
TRIAL_LICENSE_EXPIRATION_DAYS,
|
||||
dealerEmails,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
// メール送信に関する例外はログだけ出して握りつぶす
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.issueTrialLicense.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDを指定して、アカウント情報と管理者情報を取得する
|
||||
* @param context
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DataSource, IsNull } from 'typeorm';
|
||||
import {
|
||||
License,
|
||||
CardLicense,
|
||||
@ -214,3 +214,45 @@ export const getLicenseAllocationHistoryArchive = async (
|
||||
): Promise<LicenseAllocationHistoryArchive[]> => {
|
||||
return await dataSource.getRepository(LicenseAllocationHistoryArchive).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テストユーティリティ: トライアルライセンス発行数とそれに紐づく注文を取得します。
|
||||
* @param datasource データソース
|
||||
* @param fromAccountId 注文元アカウントID
|
||||
* @param toAccountId 注文先アカウントID
|
||||
* @returns licenses, orders
|
||||
*/
|
||||
export const selectIssuedLicensesAndLicenseOrders = async (
|
||||
datasource: DataSource,
|
||||
fromAccountId: number,
|
||||
toAccountId: number,
|
||||
): Promise<{
|
||||
order: LicenseOrder;
|
||||
licenses: License[];
|
||||
} | null> => {
|
||||
// 注文を取得
|
||||
const order = await datasource.getRepository(LicenseOrder).findOne({
|
||||
where: {
|
||||
from_account_id: fromAccountId,
|
||||
to_account_id: toAccountId,
|
||||
po_number: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
// 注文に紐づくライセンスを取得
|
||||
const licenses = await datasource.getRepository(License).find({
|
||||
where: {
|
||||
account_id: fromAccountId,
|
||||
order_id: order.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!licenses) return null;
|
||||
|
||||
return {
|
||||
order,
|
||||
licenses,
|
||||
};
|
||||
};
|
||||
|
||||
@ -59,6 +59,16 @@ export class GetAllocatableLicensesResponse {
|
||||
allocatableLicenses: AllocatableLicenseInfo[];
|
||||
}
|
||||
|
||||
export class IssueTrialLicenseRequest {
|
||||
@ApiProperty()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
issuedAccount: number;
|
||||
}
|
||||
|
||||
export class IssueTrialLicenseResponse {}
|
||||
|
||||
export class CancelOrderRequest {
|
||||
@ApiProperty()
|
||||
@Matches(/^[A-Z0-9]+$/)
|
||||
|
||||
@ -54,9 +54,8 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: 登録毎に新規登録する想定でUUIDを付与している
|
||||
// もしデバイスごとに登録を上書きするようであればUUID部分にデバイス識別子を設定
|
||||
const installationId = `${pns}_${userId}_${uuidv4()}`;
|
||||
// installationIdにuserIdを設定し、1ユーザー1端末の登録にする
|
||||
const installationId = `odms-user-${userId}`;
|
||||
this.logger.log(`[${context.getTrackingId()}] ${installationId}`);
|
||||
|
||||
await this.notificationhubService.register(
|
||||
|
||||
@ -132,6 +132,12 @@ export class TasksController {
|
||||
const direction = isSortDirection(body.direction ?? '')
|
||||
? (body.direction as SortDirection)
|
||||
: undefined;
|
||||
const filterConditionAuthorId = body.authorId?.trimStart()
|
||||
? body.authorId.trimStart()
|
||||
: undefined;
|
||||
const filterConditionFileName = body.fileName?.trimStart()
|
||||
? body.fileName.trimStart()
|
||||
: undefined;
|
||||
|
||||
const { tasks, total } = await this.taskService.getTasks(
|
||||
context,
|
||||
@ -143,6 +149,8 @@ export class TasksController {
|
||||
status?.split(','),
|
||||
paramName,
|
||||
direction,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
return { tasks, total, limit, offset };
|
||||
}
|
||||
@ -831,4 +839,91 @@ export class TasksController {
|
||||
await this.taskService.deleteTask(context, userId, audioFileId);
|
||||
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
@ -75,6 +75,8 @@ export class TasksService {
|
||||
status?: string[],
|
||||
paramName?: TaskListSortableAttribute,
|
||||
direction?: SortDirection,
|
||||
filterConditionAuthorId?: string | null,
|
||||
filterConditionFileName?: string | null,
|
||||
): Promise<{ tasks: Task[]; total: number }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` +
|
||||
@ -84,7 +86,10 @@ export class TasksService {
|
||||
`limit: ${limit}, ` +
|
||||
`status: ${status}, ` +
|
||||
`paramName: ${paramName}, ` +
|
||||
`direction: ${direction} };`,
|
||||
`direction: ${direction}, ` +
|
||||
`filterConditionAuthorId: ${filterConditionAuthorId},` +
|
||||
`filterConditionFileName: ${filterConditionFileName}
|
||||
};`,
|
||||
);
|
||||
|
||||
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
|
||||
@ -106,6 +111,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
|
||||
// B2Cからユーザー名を取得する
|
||||
@ -134,6 +141,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
|
||||
// B2Cからユーザー名を取得する
|
||||
@ -156,6 +165,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
// B2Cからユーザー名を取得する
|
||||
const b2cUsers = await this.getB2cUsers(
|
||||
@ -712,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する
|
||||
* @param context
|
||||
@ -1037,12 +1122,18 @@ export class TasksService {
|
||||
}
|
||||
|
||||
// タグ対象に通知送信
|
||||
await this.notificationhubService.notify(context, tags, {
|
||||
authorId: file.author_id,
|
||||
filename: file.file_name.replace('.zip', ''),
|
||||
priority: file.priority === '00' ? 'Normal' : 'High',
|
||||
uploadedAt: file.uploaded_at.toISOString(),
|
||||
});
|
||||
await Promise.all(
|
||||
tags.map((tag) => {
|
||||
return this.notificationhubService.notify(context, tag, {
|
||||
id: tag.split('user_')[1],
|
||||
authorId: file.author_id,
|
||||
filename: file.file_name.replace('.zip', ''),
|
||||
priority: file.priority === '00' ? 'Normal' : 'High',
|
||||
uploadedAt: file.uploaded_at.toISOString(),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.sendNotify.name}`,
|
||||
);
|
||||
|
||||
@ -27,6 +27,7 @@ import { NotificationhubModule } from '../../../gateways/notificationhub/notific
|
||||
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
|
||||
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../../repositories/task_filters/task_filter.repository.module';
|
||||
import { AuthService } from '../../../features/auth/auth.service';
|
||||
import { AccountsService } from '../../../features/accounts/accounts.service';
|
||||
import { UsersService } from '../../../features/users/users.service';
|
||||
@ -74,6 +75,7 @@ export const makeTaskTestingModuleWithNotificaiton = async (
|
||||
BlobstorageModule,
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
@ -110,8 +112,9 @@ export const createTask = async (
|
||||
priority: string,
|
||||
jobNumber: string,
|
||||
status: string,
|
||||
typist_user_id?: number | undefined,
|
||||
is_job_number_enabled?: boolean | undefined,
|
||||
typist_user_id?: number,
|
||||
is_job_number_enabled?: boolean,
|
||||
file_name?: string,
|
||||
): Promise<{ taskId: number; audioFileId: number }> => {
|
||||
const { identifiers: audioFileIdentifiers } = await datasource
|
||||
.getRepository(AudioFile)
|
||||
@ -119,8 +122,8 @@ export const createTask = async (
|
||||
account_id: account_id,
|
||||
owner_user_id: owner_user_id,
|
||||
url: '',
|
||||
file_name: 'x.zip',
|
||||
raw_file_name: 'y.zip',
|
||||
file_name: file_name ?? 'x.zip',
|
||||
raw_file_name: file_name ?? 'y.zip',
|
||||
author_id: author_id,
|
||||
work_type_id: work_type_id,
|
||||
started_at: new Date(),
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
@ -65,6 +66,22 @@ export class TasksRequest {
|
||||
})
|
||||
@IsOptional()
|
||||
paramName?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: `タスクの検索キーワード:AuthorID`,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
authorId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: `タスクの検索キーワード:fileName`,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
// TODO: RequestでもResponseでも使われているので、Requestに使用される箇所のみバリデータでチェックが行われる状態になっている
|
||||
|
||||
@ -12,6 +12,8 @@ import { LicensesRepositoryService } from '../../../repositories/licenses/licens
|
||||
import { UsersService } from '../users.service';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service';
|
||||
import { TaskFilters } from '../../../repositories/task_filters/entity/task_filters.entity';
|
||||
import { TaskFiltersRepositoryService } from '../../../repositories/task_filters/task_filter.repository.service';
|
||||
import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
@ -26,6 +28,11 @@ export type SortCriteriaRepositoryMockValue = {
|
||||
getSortCriteria: SortCriteria | Error;
|
||||
};
|
||||
|
||||
export type TaskFiltersRepositoryMockValue = {
|
||||
updateTaskFilter: TaskFilters | Error;
|
||||
getTaskFilter: TaskFilters | Error;
|
||||
};
|
||||
|
||||
export type UsersRepositoryMockValue = {
|
||||
updateUserVerified: undefined | Error;
|
||||
findUserById: User | Error;
|
||||
@ -63,6 +70,7 @@ export const makeUsersServiceMock = async (
|
||||
sendGridMockValue: SendGridMockValue,
|
||||
configMockValue: ConfigMockValue,
|
||||
sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue: TaskFiltersRepositoryMockValue,
|
||||
): Promise<UsersService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService],
|
||||
@ -90,6 +98,8 @@ export const makeUsersServiceMock = async (
|
||||
return makeSortCriteriaRepositoryMock(
|
||||
sortCriteriaRepositoryMockValue,
|
||||
);
|
||||
case TaskFiltersRepositoryService:
|
||||
return makeTaskFiltersRepositoryMock(taskFiltersRepositoryMockValue);
|
||||
case BlobstorageService:
|
||||
return {};
|
||||
}
|
||||
@ -128,6 +138,29 @@ export const makeSortCriteriaRepositoryMock = (
|
||||
};
|
||||
};
|
||||
|
||||
export const makeTaskFiltersRepositoryMock = (
|
||||
value: TaskFiltersRepositoryMockValue,
|
||||
) => {
|
||||
const { updateTaskFilter, getTaskFilter } = value;
|
||||
|
||||
return {
|
||||
updateTaskFilter:
|
||||
updateTaskFilter instanceof Error
|
||||
? jest
|
||||
.fn<Promise<void>, [number, string, string]>()
|
||||
.mockRejectedValue(updateTaskFilter)
|
||||
: jest
|
||||
.fn<Promise<TaskFilters>, [number, string, string]>()
|
||||
.mockResolvedValue(updateTaskFilter),
|
||||
getTaskFilter:
|
||||
getTaskFilter instanceof Error
|
||||
? jest.fn<Promise<void>, [number]>().mockRejectedValue(getTaskFilter)
|
||||
: jest
|
||||
.fn<Promise<TaskFilters>, [number]>()
|
||||
.mockResolvedValue(getTaskFilter),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeSendGridServiceMock = (value: SendGridMockValue) => {
|
||||
const { sendMail } = value;
|
||||
return {
|
||||
@ -291,6 +324,21 @@ export const makeDefaultSortCriteriaRepositoryMockValue =
|
||||
};
|
||||
};
|
||||
|
||||
export const makeDefaultTaskFiltersRepositoryMockValue =
|
||||
(): TaskFiltersRepositoryMockValue => {
|
||||
const taskFilter = new TaskFilters();
|
||||
{
|
||||
taskFilter.id = 1;
|
||||
taskFilter.author_id = null;
|
||||
taskFilter.file_name = null;
|
||||
taskFilter.user_id = 1;
|
||||
}
|
||||
return {
|
||||
updateTaskFilter: taskFilter,
|
||||
getTaskFilter: taskFilter,
|
||||
};
|
||||
};
|
||||
|
||||
export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
|
||||
return {
|
||||
getMetaData: {
|
||||
|
||||
@ -35,6 +35,15 @@ export class ConfirmRequest {
|
||||
|
||||
export class ConfirmResponse {}
|
||||
|
||||
export class ConfirmForceRequest {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export class ConfirmForceResponse {}
|
||||
|
||||
|
||||
export class User {
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
@ -84,6 +93,17 @@ export class User {
|
||||
licenseStatus: string;
|
||||
}
|
||||
|
||||
export class GetUsersRequest {
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userName?: string;
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export class GetUsersResponse {
|
||||
@ApiProperty({ type: [User] })
|
||||
users: User[];
|
||||
@ -223,6 +243,30 @@ export class GetSortCriteriaResponse {
|
||||
paramName: string;
|
||||
}
|
||||
|
||||
export class PostTaskFiltersRequest {
|
||||
@ApiProperty({ description: 'タスクの検索キーワードを更新する:AuthorID' })
|
||||
filterConditionAuthorId: string;
|
||||
|
||||
@ApiProperty({ description: 'タスクの検索キーワードを更新する:fileName' })
|
||||
filterConditionFileName: string;
|
||||
}
|
||||
|
||||
export class PostTaskFiltersResponse {}
|
||||
|
||||
export class GetTaskFiltersRequest {}
|
||||
export class GetTaskFiltersResponse {
|
||||
@ApiProperty({
|
||||
description: 'タスクの検索キーワードを取得する:AuthorID',
|
||||
required: false,
|
||||
})
|
||||
authorId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'タスクの検索キーワードを取得する:fileName',
|
||||
required: false,
|
||||
})
|
||||
fileName?: string;
|
||||
}
|
||||
export class PostUpdateUserRequest {
|
||||
@ApiProperty()
|
||||
@Type(() => Number)
|
||||
|
||||
@ -32,6 +32,10 @@ import {
|
||||
PostSortCriteriaResponse,
|
||||
GetSortCriteriaRequest,
|
||||
GetSortCriteriaResponse,
|
||||
PostTaskFiltersRequest,
|
||||
PostTaskFiltersResponse,
|
||||
GetTaskFiltersRequest,
|
||||
GetTaskFiltersResponse,
|
||||
PostUpdateUserRequest,
|
||||
PostUpdateUserResponse,
|
||||
AllocateLicenseResponse,
|
||||
@ -47,6 +51,9 @@ import {
|
||||
PostMultipleImportsResponse,
|
||||
PostMultipleImportsCompleteRequest,
|
||||
PostMultipleImportsCompleteResponse,
|
||||
ConfirmForceRequest,
|
||||
ConfirmForceResponse,
|
||||
GetUsersRequest,
|
||||
} from './types/types';
|
||||
import { UsersService } from './users.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
@ -157,6 +164,85 @@ export class UsersController {
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: ConfirmResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'メール認証済み',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'confirmUserForce',
|
||||
description: 'ユーザーを強制的にメール認証済にする',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('confirm/force')
|
||||
async confirmUserForce(
|
||||
@Body() body: ConfirmForceRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<ConfirmForceResponse> {
|
||||
const { userId } = body;
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId: loginUserId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(loginUserId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
await this.usersService.confirmUserForce(context, userId);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetUsersResponse,
|
||||
@ -179,7 +265,14 @@ export class UsersController {
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get()
|
||||
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
|
||||
async getUsers(
|
||||
@Req() req: Request,
|
||||
@Query() query: GetUsersRequest,
|
||||
): Promise<GetUsersResponse> {
|
||||
|
||||
const userName = query.userName?.trimStart();
|
||||
const email = query.email?.trimStart();
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!accessToken) {
|
||||
@ -216,7 +309,12 @@ export class UsersController {
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const users = await this.usersService.getUsers(context, userId);
|
||||
const users = await this.usersService.getUsers(
|
||||
context,
|
||||
userId,
|
||||
userName,
|
||||
email,
|
||||
);
|
||||
return { users };
|
||||
}
|
||||
|
||||
@ -535,6 +633,163 @@ export class UsersController {
|
||||
return { direction, paramName };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: PostTaskFiltersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '不正なパラメータ',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'updateTaskFilter',
|
||||
description: 'ログインしているユーザーの検索条件を更新します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('task-filters')
|
||||
async updateTaskFilter(
|
||||
@Body() body: PostTaskFiltersRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<PostTaskFiltersResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const filterConditionAuthorId = body.filterConditionAuthorId?.trimStart()
|
||||
? body.filterConditionAuthorId.trimStart()
|
||||
: null;
|
||||
const filterConditionFileName = body.filterConditionFileName?.trimStart()
|
||||
? body.filterConditionFileName.trimStart()
|
||||
: null;
|
||||
|
||||
await this.usersService.updateTaskFilter(
|
||||
context,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
userId,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetTaskFiltersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '不正なパラメータ',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'getTaskFilter',
|
||||
description: 'ログインしているユーザーのタスクの検索条件を取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('task-filters')
|
||||
async getTaskFilter(
|
||||
@Query() query: GetTaskFiltersRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<GetTaskFiltersResponse> {
|
||||
const {} = query;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const { authorId, fileName } = await this.usersService.getTaskFilter(
|
||||
context,
|
||||
userId,
|
||||
);
|
||||
return { authorId, fileName };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: PostUpdateUserResponse,
|
||||
|
||||
@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../repositories/task_filters/task_filter.repository.module';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
|
||||
import { UsersController } from './users.controller';
|
||||
@ -17,6 +18,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module
|
||||
UsersRepositoryModule,
|
||||
LicensesRepositoryModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
AdB2cModule,
|
||||
SendGridModule,
|
||||
ConfigModule,
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
makeDefaultConfigValue,
|
||||
makeDefaultSendGridlValue,
|
||||
makeDefaultSortCriteriaRepositoryMockValue,
|
||||
makeDefaultTaskFiltersRepositoryMockValue,
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeUsersServiceMock,
|
||||
} from './test/users.service.mock';
|
||||
@ -60,6 +61,7 @@ import { createCheckoutPermissions } from '../tasks/test/utility';
|
||||
import { MultipleImportErrors } from './types/types';
|
||||
import { TestLogger } from '../../common/test/logger';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { CUSTOMER_NAME } from '../../templates/constants';
|
||||
|
||||
describe('UsersService.confirmUser', () => {
|
||||
let source: DataSource | null = null;
|
||||
@ -123,7 +125,7 @@ describe('UsersService.confirmUser', () => {
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
@ -320,7 +322,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
context: Context,
|
||||
@ -892,7 +894,7 @@ describe('UsersService.createUser', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
@ -2049,6 +2051,331 @@ describe('UsersService.getUsers', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前入力メール未入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'test1', undefined);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).toBe('test1');
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前未入力メール入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, undefined, 'test2@mail.com');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].email).toBe('test2@mail.com');
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const expectedUser = {
|
||||
id: user3,
|
||||
name: 'test3',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID3',
|
||||
typistGroupName: [],
|
||||
email: 'test3@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: false,
|
||||
notification: true,
|
||||
encryption: false,
|
||||
prompt: false,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
}
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, '3', 'test3@mail');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toEqual(expectedUser);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前入力メール未入力検索で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'nonexistent', undefined);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前未入力メール入力検索で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, undefined, 'wrongemail@example.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'test1', 'wrongemail@example.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'wronguser', 'test1@mail.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前メール未入力)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
});
|
||||
|
||||
const expectedUsers = [
|
||||
{
|
||||
id: user1,
|
||||
name: 'test1',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID1',
|
||||
typistGroupName: [],
|
||||
email: 'test1@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
{
|
||||
id: user2,
|
||||
name: 'test2',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID2',
|
||||
typistGroupName: [],
|
||||
email: 'test2@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
{
|
||||
id: user3,
|
||||
name: 'test3',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID3',
|
||||
typistGroupName: [],
|
||||
email: 'test3@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
]
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result).toEqual(expectedUsers)
|
||||
});
|
||||
|
||||
it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
@ -2112,6 +2439,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
@ -2119,6 +2448,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2140,6 +2470,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
|
||||
|
||||
@ -2150,6 +2482,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2171,6 +2504,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
sortCriteriaRepositoryMockValue.updateSortCriteria = new Error(
|
||||
'sort criteria not found',
|
||||
);
|
||||
@ -2182,6 +2517,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2205,6 +2541,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
@ -2212,6 +2550,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2229,6 +2568,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
sortCriteriaRepositoryMockValue.getSortCriteria = new Error(
|
||||
'sort criteria not found',
|
||||
@ -2241,6 +2582,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2262,6 +2604,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
sortCriteriaRepositoryMockValue.getSortCriteria = {
|
||||
id: 1,
|
||||
direction: 'AAA',
|
||||
@ -2276,6 +2620,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2290,6 +2635,182 @@ describe('UsersService.getSortCriteria', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.updateTaskFilter', () => {
|
||||
it('タスク検索条件を変更できる', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
expect(
|
||||
await service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('ユーザー情報が存在せず、タスク検索条件を変更できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(
|
||||
service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスク検索条件が存在せず、タスク検索条件を変更できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
taskFiltersRepositoryMockValue.updateTaskFilter = new Error(
|
||||
'task filters not found',
|
||||
);
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(
|
||||
service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.getTaskFilter', () => {
|
||||
it('タスク検索条件を取得できる', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
console.log(await service.getTaskFilter(context, 'external_id'));
|
||||
expect(await service.getTaskFilter(context, 'external_id')).toEqual({
|
||||
authorId: undefined,
|
||||
fileName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('タスク検索条件が存在せず、タスク検索条件を取得できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
taskFiltersRepositoryMockValue.getTaskFilter = new Error(
|
||||
'task filters not found',
|
||||
);
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(service.getTaskFilter(context, 'external_id')).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.updateUser', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeAll(async () => {
|
||||
@ -5144,3 +5665,295 @@ describe('UsersService.multipleImportsComplate', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.confirmUserForce', () => {
|
||||
let source: DataSource | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (source == null) {
|
||||
source = await (async () => {
|
||||
const s = new DataSource({
|
||||
type: 'mysql',
|
||||
host: 'test_mysql_db',
|
||||
port: 3306,
|
||||
username: 'user',
|
||||
password: 'password',
|
||||
database: 'odms',
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
logger: new TestLogger('none'),
|
||||
logging: true,
|
||||
});
|
||||
return await s.initialize();
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
if (source) {
|
||||
await truncateAllTable(source);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await source?.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('第五階層の管理者がメール認証済みではないユーザーを強制認証できる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { id: user1, external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_1',
|
||||
email_verified: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
let mailSubject: string | undefined;
|
||||
let mailText: string | undefined;
|
||||
let mailTextUrl: string | undefined;
|
||||
let mailHtml: string | undefined;
|
||||
let mailHtmlUrl: string | undefined;
|
||||
let _to: string[] | undefined;
|
||||
let _cc: string[] | undefined;
|
||||
overrideSendgridService(service, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const mailTextUrls = text.match(urlPattern);
|
||||
const mailHtmlUrls = html.match(urlPattern);
|
||||
|
||||
mailSubject = subject;
|
||||
mailText = text;
|
||||
mailTextUrl = mailTextUrls?.pop();
|
||||
mailHtml = html;
|
||||
mailHtmlUrl = mailHtmlUrls?.pop();
|
||||
_to = to;
|
||||
_cc = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
// 強制認証を実行
|
||||
await service.confirmUserForce(context, user1);
|
||||
|
||||
// ユーザーのメール認証済みに変更されたことを確認
|
||||
{
|
||||
const user = await getUser(source, user1);
|
||||
if (!user) fail();
|
||||
expect(user.email_verified).toBe(true);
|
||||
}
|
||||
// メールの検証
|
||||
expect(mailSubject).toBe('Forced Email Verification Notification [U-126]');
|
||||
expect(mailText?.includes('admin')).toBe(true);
|
||||
expect(mailHtml?.includes('admin')).toBe(true);
|
||||
expect(_to).toEqual(['user1@example.com']);
|
||||
// ユーザー取得をモック化しているため、値の比較ではなく有り無しで確認
|
||||
expect(mailText?.includes(CUSTOMER_NAME)).toBe(false);
|
||||
expect(mailTextUrl).toBe('http://localhost:8081/');
|
||||
expect(_cc).not.toBeUndefined();
|
||||
expect(mailHtml?.includes(CUSTOMER_NAME)).toBe(false);
|
||||
expect(mailHtmlUrl).toBe('http://localhost:8081/');
|
||||
});
|
||||
it('存在しないユーザは強制認証できない', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
overrideSendgridService(service, {});
|
||||
try {
|
||||
await service.confirmUserForce(context, 100);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('既に認証済みのユーザは強制認証できない', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
// 認証済みユーザー
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
overrideSendgridService(service, {});
|
||||
|
||||
try {
|
||||
await service.confirmUserForce(context, admin.id);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010202'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from '../../gateways/adb2c/adb2c.service';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
|
||||
import { TaskFiltersRepositoryService } from '../../repositories/task_filters/task_filter.repository.service';
|
||||
import {
|
||||
User as EntityUser,
|
||||
newUser,
|
||||
@ -26,6 +27,7 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses.
|
||||
import {
|
||||
MultipleImportUser,
|
||||
GetRelationsResponse,
|
||||
GetTaskFiltersResponse,
|
||||
MultipleImportErrors,
|
||||
User,
|
||||
} from './types/types';
|
||||
@ -73,6 +75,7 @@ export class UsersService {
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly licensesRepository: LicensesRepositoryService,
|
||||
private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
|
||||
private readonly taskFiltersRepository: TaskFiltersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
@ -603,10 +606,18 @@ export class UsersService {
|
||||
|
||||
/**
|
||||
* Get Users
|
||||
* @param accessToken
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param userInputUserName
|
||||
* @param userInputEmail
|
||||
* @returns users
|
||||
*/
|
||||
async getUsers(context: Context, externalId: string): Promise<User[]> {
|
||||
async getUsers(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
userInputUserName?: string,
|
||||
userInputEmail?: string,
|
||||
): Promise<User[]> {
|
||||
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
|
||||
|
||||
try {
|
||||
@ -617,7 +628,7 @@ export class UsersService {
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
const externalIds = dbUsers.map((x) => x.external_id);
|
||||
const externalIds = dbUsers.map((user) => user.external_id);
|
||||
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
|
||||
|
||||
// DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
|
||||
@ -703,7 +714,22 @@ export class UsersService {
|
||||
};
|
||||
});
|
||||
|
||||
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) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
@ -831,6 +857,116 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task filters
|
||||
* @param authorId
|
||||
* @param fileName
|
||||
* @param token
|
||||
* @returns task filters
|
||||
*/
|
||||
async updateTaskFilter(
|
||||
context: Context,
|
||||
filterConditionAuthorId: string | null,
|
||||
filterConditionFileName: string | null,
|
||||
externalId: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.updateTaskFilter.name
|
||||
} | params: { filterConditionAuthorId: ${filterConditionAuthorId}, filterConditionFileName: ${filterConditionFileName}, externalId: ${externalId} };`,
|
||||
);
|
||||
let user: EntityUser;
|
||||
try {
|
||||
// ユーザー情報を取得
|
||||
user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// ユーザーの検索条件を更新
|
||||
await this.taskFiltersRepository.updateTaskFilter(
|
||||
user.id,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
context,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.updateTaskFilter.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets task filters
|
||||
* @param token
|
||||
* @returns task filters
|
||||
*/
|
||||
async getTaskFilter(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
): Promise<GetTaskFiltersResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.getTaskFilter.name
|
||||
} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
let user: EntityUser;
|
||||
try {
|
||||
// ユーザー情報を取得
|
||||
user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// ユーザーのタスク検索条件を取得
|
||||
const taskFilters = await this.taskFiltersRepository.getTaskFilter(
|
||||
user.id,
|
||||
context,
|
||||
);
|
||||
const { author_id: authorId, file_name: fileName } = taskFilters;
|
||||
const result = {
|
||||
authorId: authorId ?? undefined,
|
||||
fileName: fileName ?? undefined,
|
||||
};
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.getTaskFilter.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーの文字起こし業務に関連する情報を取得します
|
||||
* @param userId
|
||||
@ -1740,6 +1876,99 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーの強制認証
|
||||
* @param userId ユーザId
|
||||
*/
|
||||
async confirmUserForce(context: Context, userId: number): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
|
||||
);
|
||||
try {
|
||||
// ユーザーをメール認証済みにする。
|
||||
await this.usersRepository.updateUserVerified(context, userId);
|
||||
|
||||
// 通知先のユーザーを取得
|
||||
const { external_id, account_id } =
|
||||
await this.usersRepository.findUserById(context, userId);
|
||||
|
||||
const adb2cUser = await this.adB2cService.getUser(context, external_id);
|
||||
const { displayName, emailAddress } =
|
||||
getUserNameAndMailAddress(adb2cUser);
|
||||
|
||||
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
|
||||
if (!emailAddress) {
|
||||
throw new UserNotFoundError(
|
||||
`emailAddress is null. externalId=${external_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// プライマリアカウント管理者を取得する
|
||||
const { primary_admin_user_id } =
|
||||
await this.accountsRepository.findAccountById(context, account_id);
|
||||
|
||||
if (primary_admin_user_id === null) {
|
||||
throw new UserNotFoundError(
|
||||
`primary_admin_user_id is null. account_id=${account_id}`,
|
||||
);
|
||||
}
|
||||
const { external_id: primaryUserExtarnalId } =
|
||||
await this.usersRepository.findUserById(context, primary_admin_user_id);
|
||||
|
||||
const primaryAdmimAdb2cUser = await this.adB2cService.getUser(
|
||||
context,
|
||||
primaryUserExtarnalId,
|
||||
);
|
||||
const {
|
||||
emailAddress: primaryAdminMailAdress,
|
||||
} = getUserNameAndMailAddress(primaryAdmimAdb2cUser);
|
||||
|
||||
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
|
||||
if (!primaryAdminMailAdress) {
|
||||
throw new UserNotFoundError(
|
||||
`primary admin emailAddress is null. externalId=${primaryUserExtarnalId}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// アカウント認証が完了した旨をメール送信する
|
||||
await this.sendgridService.sendMailWithU126(
|
||||
context,
|
||||
emailAddress,
|
||||
displayName,
|
||||
primaryAdminMailAdress,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
// メール送信に関する例外はログだけ出して握りつぶす
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case EmailAlreadyVerifiedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010202'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDを指定して、アカウント情報と管理者情報を取得する
|
||||
* @param context
|
||||
|
||||
@ -520,8 +520,8 @@ export class BlobstorageService {
|
||||
this.getContainerClient.name
|
||||
} | params: { ` + `accountId: ${accountId} };`,
|
||||
);
|
||||
|
||||
const containerName = `account-${accountId}`;
|
||||
|
||||
if (BLOB_STORAGE_REGION_US.includes(country)) {
|
||||
return this.blobServiceClientUS.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
createAppleInstallation,
|
||||
createWindowsRawNotification,
|
||||
} from '@azure/notification-hubs';
|
||||
import { TAG_MAX_COUNT } from '../../constants';
|
||||
import { PNS } from '../../constants';
|
||||
import { Context } from '../../common/log';
|
||||
import { NotificationBody } from '../../common/notify/types/types';
|
||||
@ -83,65 +82,57 @@ export class NotificationhubService {
|
||||
/**
|
||||
* 指定したタグのユーザーに通知を送信する
|
||||
* @param context
|
||||
* @param tags
|
||||
* @param tag
|
||||
* @param bodyContent
|
||||
* @returns notify
|
||||
*/
|
||||
async notify(
|
||||
context: Context,
|
||||
tags: string[],
|
||||
tag: string,
|
||||
bodyContent: NotificationBody,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.notify.name
|
||||
} | params: { tags: ${tags}, bodyContent: ${JSON.stringify(
|
||||
bodyContent,
|
||||
)} }`,
|
||||
} | params: { tag: ${tag}, bodyContent: ${JSON.stringify(bodyContent)} }`,
|
||||
);
|
||||
|
||||
try {
|
||||
// OR条件によるtag指定は20個までなので分割して送信する
|
||||
const chunkTags = splitArrayInChunks(tags, TAG_MAX_COUNT);
|
||||
const tagExpression = createTagExpression([tag]);
|
||||
|
||||
for (let index = 0; index < chunkTags.length; index++) {
|
||||
const currentTags = chunkTags[index];
|
||||
const tagExpression = createTagExpression(currentTags);
|
||||
|
||||
// Windows
|
||||
try {
|
||||
const body = {
|
||||
wns: {
|
||||
alert: '',
|
||||
},
|
||||
newDictation: bodyContent,
|
||||
};
|
||||
const notification = createWindowsRawNotification({
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const result = await this.client.sendNotification(notification, {
|
||||
tagExpression,
|
||||
});
|
||||
this.logger.log(`[${context.getTrackingId()}] ${result}`);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
}
|
||||
// Apple
|
||||
try {
|
||||
const body = createAppleNotificationBody({
|
||||
aps: {
|
||||
alert: '',
|
||||
},
|
||||
newDictation: bodyContent,
|
||||
});
|
||||
const notification = createAppleNotification({ body });
|
||||
const result = await this.client.sendNotification(notification, {
|
||||
tagExpression,
|
||||
});
|
||||
this.logger.log(`[${context.getTrackingId()}] ${result}`);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
}
|
||||
// Windows
|
||||
try {
|
||||
const body = {
|
||||
wns: {
|
||||
alert: '',
|
||||
},
|
||||
newDictation: bodyContent,
|
||||
};
|
||||
const notification = createWindowsRawNotification({
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const result = await this.client.sendNotification(notification, {
|
||||
tagExpression,
|
||||
});
|
||||
this.logger.log(`[${context.getTrackingId()}] ${result}`);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
}
|
||||
// Apple
|
||||
try {
|
||||
const body = createAppleNotificationBody({
|
||||
aps: {
|
||||
alert: '',
|
||||
},
|
||||
newDictation: bodyContent,
|
||||
});
|
||||
const notification = createAppleNotification({ body });
|
||||
const result = await this.client.sendNotification(notification, {
|
||||
tagExpression,
|
||||
});
|
||||
this.logger.log(`[${context.getTrackingId()}] ${result}`);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
}
|
||||
} catch (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;
|
||||
};
|
||||
|
||||
@ -33,6 +33,8 @@ import {
|
||||
NO_ERROR_MESSAGE_EN,
|
||||
NO_ERROR_MESSAGE_DE,
|
||||
NO_ERROR_MESSAGE_FR,
|
||||
ISSUER_CUSTOMER_NAME,
|
||||
EXPIRATION_DATE,
|
||||
} from '../../templates/constants';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
@ -100,6 +102,10 @@ export class SendGridService {
|
||||
private readonly templateU123Text: string;
|
||||
private readonly templateU124Html: string;
|
||||
private readonly templateU124Text: string;
|
||||
private readonly templateU125Html: string;
|
||||
private readonly templateU125Text: string;
|
||||
private readonly templateU126Html: string;
|
||||
private readonly templateU126Text: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
|
||||
@ -357,6 +363,22 @@ export class SendGridService {
|
||||
path.resolve(__dirname, `../../templates/template_U_124.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU125Html = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_125.html`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU125Text = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_125.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU126Html = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_126.html`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU126Text = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_126.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,11 +401,11 @@ export class SendGridService {
|
||||
const subject = 'Account Registered Notification [U-101]';
|
||||
const url = new URL(this.appDomain).href;
|
||||
const html = this.templateU101Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
const text = this.templateU101Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
|
||||
await this.sendMail(
|
||||
context,
|
||||
@ -434,8 +456,8 @@ export class SendGridService {
|
||||
const verifyUrl = `${url}?verify=${token}`;
|
||||
|
||||
const subject = 'User Registration Notification [U-102]';
|
||||
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl);
|
||||
const text = this.templateU102Text.replaceAll(VERIFY_LINK, verifyUrl);
|
||||
const html = this.templateU102Html.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
|
||||
const text = this.templateU102Text.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
|
||||
|
||||
await this.sendMail(
|
||||
context,
|
||||
@ -481,15 +503,15 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU105Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
const text = this.templateU105Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -536,15 +558,15 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU106Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
const text = this.templateU106Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -591,15 +613,15 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU107Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
|
||||
const text = this.templateU107Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -648,28 +670,28 @@ export class SendGridService {
|
||||
|
||||
if (dealerAccountName === null) {
|
||||
html = this.templateU108NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = this.templateU108NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
} else {
|
||||
html = this.templateU108Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = this.templateU108Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(USER_EMAIL, userMail)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(USER_EMAIL, escapeDollar(userMail))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
}
|
||||
|
||||
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
|
||||
@ -719,15 +741,15 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU109Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
const text = this.templateU109Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PO_NUMBER, poNumber)
|
||||
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -769,14 +791,14 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU111Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
|
||||
const text = this.templateU111Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -825,24 +847,24 @@ export class SendGridService {
|
||||
if (dealerAccountName === null) {
|
||||
// メールの本文を作成する
|
||||
html = this.templateU112NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = this.templateU112NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
} else {
|
||||
html = this.templateU112Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
text = this.templateU112Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -884,11 +906,11 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU113Html
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword);
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
|
||||
const text = this.templateU113Text
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword);
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -947,11 +969,11 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU114Html
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(VERIFY_LINK, verifyUrl);
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
|
||||
const text = this.templateU114Text
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
|
||||
.replaceAll(VERIFY_LINK, verifyUrl);
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
|
||||
.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -994,11 +1016,11 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU115Html
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
|
||||
const text = this.templateU115Text
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
|
||||
|
||||
// 管理者ユーザーの情報を変更した場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない
|
||||
const ccAdminMails = adminMails.filter((x) => x !== userMail);
|
||||
@ -1044,11 +1066,11 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU116Html
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
|
||||
const text = this.templateU116Text
|
||||
.replaceAll(USER_NAME, userName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
|
||||
.replaceAll(USER_NAME, escapeDollar(userName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -1094,15 +1116,15 @@ export class SendGridService {
|
||||
|
||||
// メールの本文を作成する
|
||||
const html = this.templateU117Html
|
||||
.replaceAll(AUTHOR_NAME, authorName)
|
||||
.replaceAll(FILE_NAME, fileName)
|
||||
.replaceAll(TYPIST_NAME, typistName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, adminName);
|
||||
.replaceAll(AUTHOR_NAME, escapeDollar(authorName))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName))
|
||||
.replaceAll(TYPIST_NAME, escapeDollar(typistName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(adminName));
|
||||
const text = this.templateU117Text
|
||||
.replaceAll(AUTHOR_NAME, authorName)
|
||||
.replaceAll(FILE_NAME, fileName)
|
||||
.replaceAll(TYPIST_NAME, typistName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, adminName);
|
||||
.replaceAll(AUTHOR_NAME, escapeDollar(authorName))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName))
|
||||
.replaceAll(TYPIST_NAME, escapeDollar(typistName))
|
||||
.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);
|
||||
@ -1144,20 +1166,19 @@ export class SendGridService {
|
||||
|
||||
if (!dealerAccountName) {
|
||||
html = this.templateU118NoParentHtml.replaceAll(
|
||||
CUSTOMER_NAME,
|
||||
customerAccountName,
|
||||
CUSTOMER_NAME,escapeDollar( customerAccountName),
|
||||
);
|
||||
text = this.templateU118NoParentText.replaceAll(
|
||||
CUSTOMER_NAME,
|
||||
customerAccountName,
|
||||
escapeDollar(customerAccountName),
|
||||
);
|
||||
} else {
|
||||
html = this.templateU118Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
text = this.templateU118Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -1205,19 +1226,19 @@ export class SendGridService {
|
||||
if (!dealerAccountName) {
|
||||
html = this.templateU119NoParentHtml.replaceAll(
|
||||
CUSTOMER_NAME,
|
||||
customerAccountName,
|
||||
escapeDollar(customerAccountName),
|
||||
);
|
||||
text = this.templateU119NoParentText.replaceAll(
|
||||
CUSTOMER_NAME,
|
||||
customerAccountName,
|
||||
escapeDollar(customerAccountName),
|
||||
);
|
||||
} else {
|
||||
html = this.templateU119Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
text = this.templateU119Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -1266,24 +1287,24 @@ export class SendGridService {
|
||||
|
||||
if (!dealerAccountName) {
|
||||
html = this.templateU120NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
text = this.templateU120NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
} else {
|
||||
html = this.templateU120Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
text = this.templateU120Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -1332,24 +1353,24 @@ export class SendGridService {
|
||||
|
||||
if (!dealerAccountName) {
|
||||
html = this.templateU121NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
text = this.templateU121NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
} else {
|
||||
html = this.templateU121Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
text = this.templateU121Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(REQUEST_TIME, requestTime)
|
||||
.replaceAll(FILE_NAME, fileName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
|
||||
.replaceAll(FILE_NAME, escapeDollar(fileName));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -1432,52 +1453,52 @@ export class SendGridService {
|
||||
|
||||
if (!dealerAccountName) {
|
||||
html = this.templateU122NoParentHtml
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
|
||||
text = this.templateU122NoParentText
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
|
||||
} else {
|
||||
html = this.templateU122Html
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
|
||||
text = this.templateU122Text
|
||||
.replaceAll(CUSTOMER_NAME, customerAccountName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
|
||||
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
|
||||
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
|
||||
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
|
||||
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
|
||||
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
|
||||
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
|
||||
}
|
||||
|
||||
// メールを送信する
|
||||
@ -1522,13 +1543,13 @@ export class SendGridService {
|
||||
const subject = 'Partner Account Deleted Notification [U-123]';
|
||||
|
||||
const html = this.templateU123Html
|
||||
.replaceAll(CUSTOMER_NAME, partnerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
const text = this.templateU123Text
|
||||
.replaceAll(CUSTOMER_NAME, partnerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -1573,15 +1594,15 @@ export class SendGridService {
|
||||
const url = new URL(this.appDomain).href;
|
||||
|
||||
const html = this.templateU124Html
|
||||
.replaceAll(CUSTOMER_NAME, partnerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
const text = this.templateU124Text
|
||||
.replaceAll(CUSTOMER_NAME, partnerAccountName)
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
|
||||
.replaceAll(DEALER_NAME, dealerAccountName)
|
||||
.replaceAll(TOP_URL, url);
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
|
||||
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
|
||||
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
@ -1600,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
|
||||
@ -1657,3 +1776,9 @@ export class SendGridService {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 与えられた文字列内の $ を $$ にエスケープする
|
||||
* @param str - エスケープする対象の文字列
|
||||
* @returns エスケープ後の文字列
|
||||
*/
|
||||
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");
|
||||
@ -1,5 +1,5 @@
|
||||
import sendgrid from '@sendgrid/mail';
|
||||
import { SendGridService } from './sendgrid.service';
|
||||
import { escapeDollar, SendGridService } from './sendgrid.service';
|
||||
import { Context, makeContext } from '../../common/log';
|
||||
|
||||
// sendgridのsend関数をモック化
|
||||
@ -131,3 +131,111 @@ describe('SendGridService', () => {
|
||||
).rejects.toThrow('Send failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeDollar', () => {
|
||||
test('文字列に$が含まれていない場合はそのまま返す', () => {
|
||||
expect(escapeDollar('Hello World')).toBe('Hello World');
|
||||
expect(escapeDollar('No dollars here!')).toBe('No dollars here!');
|
||||
expect(escapeDollar('')).toBe('');
|
||||
});
|
||||
|
||||
test('$が文字列の先頭にある場合、エスケープされる', () => {
|
||||
expect(escapeDollar('$Hello')).toBe('$$Hello');
|
||||
});
|
||||
|
||||
test('$が文字列の末尾にある場合、エスケープされる', () => {
|
||||
expect(escapeDollar('World$')).toBe('World$$');
|
||||
});
|
||||
|
||||
test('$が文字列の途中にある場合、エスケープされる', () => {
|
||||
expect(escapeDollar('Hello$World')).toBe('Hello$$World');
|
||||
});
|
||||
|
||||
test('複数の$が文字列に含まれる場合、すべてエスケープされる', () => {
|
||||
expect(escapeDollar('$$Hello$$World$$')).toBe('$$$$Hello$$$$World$$$$');
|
||||
expect(escapeDollar('$1$2$3')).toBe('$$1$$2$$3');
|
||||
});
|
||||
|
||||
test('特殊文字と$が混在する場合でも$が正しくエスケープされる', () => {
|
||||
expect(escapeDollar('Price: $5, Tax: $0.50')).toBe(
|
||||
'Price: $$5, Tax: $$0.50',
|
||||
);
|
||||
expect(escapeDollar('Special.$World$^$')).toBe('Special.$$World$$^$$');
|
||||
});
|
||||
|
||||
test('文字列が$だけの場合、すべてエスケープされる', () => {
|
||||
expect(escapeDollar('$')).toBe('$$');
|
||||
expect(escapeDollar('$$$')).toBe('$$$$$$');
|
||||
});
|
||||
|
||||
test('すでにエスケープされた$が含まれる場合、さらにエスケープされる', () => {
|
||||
expect(escapeDollar('$$Already$$Escaped$$')).toBe(
|
||||
'$$$$Already$$$$Escaped$$$$',
|
||||
);
|
||||
});
|
||||
|
||||
test('空白や改行、タブ文字を含む文字列でも$がエスケープされる', () => {
|
||||
expect(escapeDollar(' $Leading$Trailing$ ')).toBe(
|
||||
' $$Leading$$Trailing$$ ',
|
||||
);
|
||||
expect(escapeDollar('Line1$\nLine2$\tEnd$')).toBe(
|
||||
'Line1$$\nLine2$$\tEnd$$',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('特殊文字を含む置き換え処理でescapeDollarを使用するテスト', () => {
|
||||
test('escapeDollarを使わない場合と使った場合の置き換え結果を比較', () => {
|
||||
const input = 'This is $TEMPORARY_PASSWORD$ for you.';
|
||||
const replacement = 'T$&AS$123$';
|
||||
|
||||
// escapeDollarを使わない場合
|
||||
const resultWithoutEscape = input.replaceAll(
|
||||
'$TEMPORARY_PASSWORD$',
|
||||
replacement,
|
||||
);
|
||||
|
||||
// escapeDollarを使った場合
|
||||
const safeReplacement = escapeDollar(replacement);
|
||||
const resultWithEscape = input.replaceAll(
|
||||
'$TEMPORARY_PASSWORD$',
|
||||
safeReplacement,
|
||||
);
|
||||
|
||||
// 意図しない結果になることを確認
|
||||
expect(resultWithoutEscape).not.toBe('This is T$&AS$123$ for you.');
|
||||
expect(resultWithoutEscape).toContain('$TEMPORARY_PASSWORD$'); // 意図通りに置換されていない場合が含まれる
|
||||
|
||||
// escapeDollarを使用した結果が意図通りであることを確認
|
||||
expect(resultWithEscape).toBe('This is T$&AS$123$ for you.');
|
||||
});
|
||||
|
||||
test("特殊文字 ($1, $`, $') を含む場合の置換処理を確認", () => {
|
||||
const input = 'This is $TEMPORARY_PASSWORD$ for you.';
|
||||
const replacementWithGroup = 'Group-$1-Value';
|
||||
const replacementWithBacktick = 'Backtick-$`-Value';
|
||||
const replacementWithSingleQuote = "Single-$'-Value";
|
||||
|
||||
// $1: キャプチャグループ
|
||||
const resultWithGroup = input.replaceAll(
|
||||
'$TEMPORARY_PASSWORD$',
|
||||
escapeDollar(replacementWithGroup),
|
||||
);
|
||||
expect(resultWithGroup).toBe('This is Group-$1-Value for you.');
|
||||
|
||||
// $`: 置換対象文字列の先頭部分
|
||||
const resultWithBacktick = input.replaceAll(
|
||||
'$TEMPORARY_PASSWORD$',
|
||||
escapeDollar(replacementWithBacktick),
|
||||
);
|
||||
expect(resultWithBacktick).toBe('This is Backtick-$`-Value for you.');
|
||||
|
||||
// $': 置換対象文字列の後続部分
|
||||
const resultWithSingleQuote = input.replaceAll(
|
||||
'$TEMPORARY_PASSWORD$',
|
||||
escapeDollar(replacementWithSingleQuote),
|
||||
);
|
||||
expect(resultWithSingleQuote).toBe("This is Single-$'-Value for you.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Between,
|
||||
@ -9,6 +10,8 @@ import {
|
||||
Not,
|
||||
UpdateResult,
|
||||
EntityManager,
|
||||
FindOptionsWhere,
|
||||
Like,
|
||||
} from 'typeorm';
|
||||
import { User, UserArchive } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
@ -21,6 +24,8 @@ import {
|
||||
LicenseOrder,
|
||||
} from '../licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../task_filters/entity/task_filters.entity';
|
||||
import { escapeLikeString } from '../../common/repository/utils/utils';
|
||||
import {
|
||||
getDirection,
|
||||
getTaskListSortableAttribute,
|
||||
@ -66,6 +71,7 @@ import {
|
||||
LicenseSummaryInfo,
|
||||
PartnerInfoFromDb,
|
||||
PartnerLicenseInfoForRepository,
|
||||
SearchPartnerInfoFromDb,
|
||||
} from '../../features/accounts/types/types';
|
||||
import { AccountArchive } from './entity/account_archive.entity';
|
||||
import { JobNumber } from '../job_number/entity/job_number.entity';
|
||||
@ -242,6 +248,21 @@ export class AccountsRepositoryService {
|
||||
context,
|
||||
);
|
||||
|
||||
// ユーザーのタスク検索条件を作成
|
||||
const taskFilters = new TaskFilters();
|
||||
{
|
||||
taskFilters.user_id = persistedUser.id;
|
||||
}
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
const newTaskFilters = taskFiltersRepo.create(taskFilters);
|
||||
await insertEntity(
|
||||
TaskFilters,
|
||||
taskFiltersRepo,
|
||||
newTaskFilters,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return { newAccount: persistedAccount, adminUser: persistedUser };
|
||||
});
|
||||
}
|
||||
@ -260,6 +281,7 @@ export class AccountsRepositoryService {
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
const jobNumberRepo = entityManager.getRepository(JobNumber);
|
||||
// JobNumberを削除
|
||||
await deleteEntity(
|
||||
@ -275,6 +297,13 @@ export class AccountsRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// タスク検索条件を削除
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: userId },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// プライマリ管理者を削除
|
||||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||||
// アカウントを削除
|
||||
@ -411,6 +440,35 @@ export class AccountsRepositoryService {
|
||||
return expiringSoonLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||||
* 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
* @param entityManager
|
||||
* @param id
|
||||
* @param currentDate
|
||||
* @returns expiringSoonLicense
|
||||
*/
|
||||
private async getAllocatedLicense(
|
||||
context: Context,
|
||||
entityManager: EntityManager,
|
||||
id: number,
|
||||
currentDate: Date,
|
||||
): Promise<number> {
|
||||
const license = entityManager.getRepository(License);
|
||||
|
||||
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
const allocatedLicense = await license.count({
|
||||
where: {
|
||||
account_id: id,
|
||||
expiry_date: MoreThanOrEqual(currentDate),
|
||||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
return allocatedLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||||
* 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||||
@ -497,14 +555,12 @@ export class AccountsRepositoryService {
|
||||
});
|
||||
|
||||
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
const allocatedLicense = await license.count({
|
||||
where: {
|
||||
account_id: id,
|
||||
expiry_date: MoreThanOrEqual(currentDate),
|
||||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
const allocatedLicense = await this.getAllocatedLicense(
|
||||
context,
|
||||
entityManager,
|
||||
id,
|
||||
currentDate,
|
||||
);
|
||||
|
||||
// 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
|
||||
const reusableLicense = await license.count({
|
||||
@ -749,6 +805,7 @@ export class AccountsRepositoryService {
|
||||
// 第五の不足数を算出するためのライセンス数情報を取得する
|
||||
let expiringSoonLicense: number = 0; // eslint-disable-line
|
||||
let allocatableLicenseWithMargin: number = 0; // eslint-disable-line
|
||||
let allocatedLicense: number | undefined = undefined; // eslint-disable-line
|
||||
if (childAccount.tier === TIERS.TIER5) {
|
||||
expiringSoonLicense = await this.getExpiringSoonLicense(
|
||||
context,
|
||||
@ -764,6 +821,12 @@ export class AccountsRepositoryService {
|
||||
childAccount.id,
|
||||
expiringSoonDate,
|
||||
);
|
||||
allocatedLicense = await this.getAllocatedLicense(
|
||||
context,
|
||||
entityManager,
|
||||
childAccount.id,
|
||||
currentDate,
|
||||
);
|
||||
}
|
||||
|
||||
// 戻り値用の値を設定
|
||||
@ -777,6 +840,7 @@ export class AccountsRepositoryService {
|
||||
issueRequesting: childLicenseOrderStatus.issueRequesting,
|
||||
expiringSoonLicense: expiringSoonLicense,
|
||||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||||
allocatedLicense: allocatedLicense,
|
||||
};
|
||||
|
||||
childPartnerLicensesFromRepository.push(
|
||||
@ -842,10 +906,15 @@ export class AccountsRepositoryService {
|
||||
if (!account) {
|
||||
break;
|
||||
}
|
||||
if (!account.parent_account_id) {
|
||||
throw new Error("Parent account doesn't exist.");
|
||||
// 第一階層はアカウント階層の最上位であるため、到達したらループを抜ける
|
||||
if (account.tier === TIERS.TIER1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 第二〜第五のアカウントには親アカウントIDがかならず設定されている
|
||||
assert(
|
||||
account.parent_account_id,
|
||||
new Error("Parent account doesn't exist."),
|
||||
);
|
||||
parentAccountIds.push(account.parent_account_id);
|
||||
currentAccountId = account.parent_account_id;
|
||||
}
|
||||
@ -1441,6 +1510,16 @@ export class AccountsRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件のテーブルのレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: In(users.map((user) => user.id)) },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return users;
|
||||
});
|
||||
}
|
||||
@ -1657,6 +1736,15 @@ export class AccountsRepositoryService {
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件のレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: In(users.map((user) => user.id)) },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// JobNumberのテーブルのレコードを削除する
|
||||
const jobNumberRepo = entityManager.getRepository(JobNumber);
|
||||
await deleteEntity(
|
||||
@ -1881,4 +1969,209 @@ export class AccountsRepositoryService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自身のアカウントに関連するアカウントを取得する
|
||||
* @param context
|
||||
* @param ownAccountId
|
||||
* @param ownAccountTier
|
||||
* @param primaryAdminUserId
|
||||
* @param companyName
|
||||
* @param targetAccountId
|
||||
* @returns partner info
|
||||
*/
|
||||
async getAccountsRelatedOwnAccount(
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownAccountTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
): Promise<SearchPartnerInfoFromDb[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const whereClause: FindOptionsWhere<Account> = {
|
||||
// 共通の条件として、自分の階層より下に限定する
|
||||
tier: MoreThan(ownAccountTier)
|
||||
};
|
||||
|
||||
// 検索条件に入力があればAND条件で追加
|
||||
if (targetAccountId) {
|
||||
whereClause.id = targetAccountId;
|
||||
}
|
||||
if (companyName) {
|
||||
whereClause.company_name = Like(`%${escapeLikeString(companyName)}%`);
|
||||
}
|
||||
|
||||
const accountsRepository = entityManager.getRepository(Account);
|
||||
const filterdAccounts = await accountsRepository.find({
|
||||
where: whereClause,
|
||||
order: {
|
||||
tier: 'DESC',
|
||||
company_name: 'ASC'
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 親アカウントIDが無いアカウントは検索結果から弾く
|
||||
const parentAccountIds = filterdAccounts.filter(
|
||||
(
|
||||
account,
|
||||
): account is NonNullable<
|
||||
typeof account & { parent_account_id: number }
|
||||
> => account.parent_account_id !== null,
|
||||
);
|
||||
|
||||
// 親子関係にあるアカウントのみを検索対象にする
|
||||
// 階層構造を辿っていく回数を少なくするため、親子関係有り無しのキャッシュセットを作成しておく
|
||||
let relatedAccountCache: Set<number> = new Set();
|
||||
let notRelatedAccountCache: Set<number> = new Set();
|
||||
|
||||
// 親子関係にあるアカウントかどうかをチェックする
|
||||
const relatedAccounts = await Promise.all(
|
||||
parentAccountIds.map((account) => {
|
||||
return this.checkRelatedAccount(
|
||||
context,
|
||||
ownAccountId,
|
||||
ownAccountTier,
|
||||
account,
|
||||
account.parent_account_id,
|
||||
account.tier,
|
||||
relatedAccountCache,
|
||||
notRelatedAccountCache,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// アカウント階層構造のキャッシュをクリアする。
|
||||
// nullに書き換えるために強制的にunkown型に変換。
|
||||
relatedAccountCache.clear();
|
||||
(relatedAccountCache as unknown) = null;
|
||||
notRelatedAccountCache.clear();
|
||||
(notRelatedAccountCache as unknown) = null;
|
||||
|
||||
// 親子関係ではないアカウントを取り除き、階層順でソート
|
||||
const relatedAccountsNonNull = relatedAccounts
|
||||
.filter(
|
||||
(account): account is NonNullable<typeof account> => account !== null,
|
||||
)
|
||||
.sort((a, b) => a.tier - b.tier);
|
||||
|
||||
// 検索できていなければ、空の検索結果を返す
|
||||
if (relatedAccountsNonNull.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮)
|
||||
const primaryUserIds = relatedAccountsNonNull.flatMap((x) => {
|
||||
if (x.primary_admin_user_id) {
|
||||
return [x.primary_admin_user_id];
|
||||
} else if (x.secondary_admin_user_id) {
|
||||
return [x.secondary_admin_user_id];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUsers = await userRepo.find({
|
||||
where: {
|
||||
id: In(primaryUserIds),
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// アカウント情報とプライマリ管理者の外部ユーザIDをマージ
|
||||
const partners = relatedAccountsNonNull.map((account) => {
|
||||
const primaryUser = primaryUsers.find(
|
||||
(user) =>
|
||||
// Raw SQLで取得できた管理者ユーザーIDは文字列になっているので、厳密等価で比較しない
|
||||
user.id == account.primary_admin_user_id ||
|
||||
user.id == account.secondary_admin_user_id,
|
||||
);
|
||||
if (!primaryUser) {
|
||||
throw new AdminUserNotFoundError(
|
||||
`Primary admin user is not found. id: ${account.primary_admin_user_id}, account_id: ${account.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: account.company_name,
|
||||
tier: account.tier,
|
||||
accountId: account.id,
|
||||
country: account.country,
|
||||
primaryAccountExternalId: primaryUser.external_id,
|
||||
};
|
||||
});
|
||||
|
||||
return partners;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* targetAccountが自身のアカウントと親子関係になっているかをチェックし、親子間家になっていればtargetAccountを返す
|
||||
* @param context
|
||||
* @param ownAccountId
|
||||
* @param ownTier
|
||||
* @param targetAccount
|
||||
* @param parentAccountId
|
||||
* @param tier
|
||||
* @param relatedAccountCache
|
||||
* @param notRelatedAccountCache
|
||||
* @returns
|
||||
*/
|
||||
private async checkRelatedAccount(
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownTier: number,
|
||||
targetAccount: Account,
|
||||
parentAccountId: number,
|
||||
tier: number,
|
||||
relatedAccountCache: Set<number>,
|
||||
notRelatedAccountCache: Set<number>,
|
||||
): Promise<Account | null> {
|
||||
// 親子関係のアカウントの場合、DBにアクセスせずに検索結果に含める
|
||||
if (Array.from(relatedAccountCache).includes(parentAccountId)) {
|
||||
return targetAccount;
|
||||
}
|
||||
|
||||
// 親子関係のアカウントではない場合、DBにアクセスせずに検索結果から除外する
|
||||
if (Array.from(notRelatedAccountCache).includes(parentAccountId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentAccount = await this.getOneUpperTierAccount(
|
||||
context,
|
||||
parentAccountId,
|
||||
tier,
|
||||
);
|
||||
|
||||
// 通常ありえないが、親を持たないアカウントの場合は検索対象としない。
|
||||
if (!parentAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentAccount.tier !== ownTier) {
|
||||
// 検索者の階層と一致しない場合は、上位の階層を検索する。
|
||||
return this.checkRelatedAccount(
|
||||
context,
|
||||
ownAccountId,
|
||||
ownTier,
|
||||
targetAccount,
|
||||
parentAccount.parent_account_id!,
|
||||
parentAccount.tier,
|
||||
relatedAccountCache,
|
||||
notRelatedAccountCache,
|
||||
);
|
||||
}
|
||||
|
||||
if (parentAccount.id !== ownAccountId) {
|
||||
// 検索者の階層と一致しているかつ、アカウントIDが一致しない場合は、検索対象としない。
|
||||
// 親子関係ではないリストに情報をキャッシュ
|
||||
notRelatedAccountCache.add(parentAccount.id);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 検索者の階層と一致しているかつ、アカウントIDが一致する場合は、検索対象に含める。
|
||||
// 親子関係リストに情報をキャッシュ
|
||||
relatedAccountCache.add(parentAccount.id);
|
||||
return targetAccount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,9 @@ export class LicenseOrder {
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
issued_at: Date | null;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
quantity: number;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
||||
import { DataSource, In, MoreThanOrEqual } from 'typeorm';
|
||||
import {
|
||||
LicenseOrder,
|
||||
License,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from './entity/license.entity';
|
||||
import {
|
||||
CARD_LICENSE_LENGTH,
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
LICENSE_TYPE,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
import {
|
||||
AllocatableLicenseInfo,
|
||||
DateWithZeroTime,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from '../../features/licenses/types/types';
|
||||
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
|
||||
import {
|
||||
@ -514,6 +516,66 @@ export class LicensesRepositoryService {
|
||||
return { issuedOrderId: issuingOrder.id };
|
||||
});
|
||||
}
|
||||
/**
|
||||
* トライアルライセンスを発行する。ライセンスに紐づくライセンス注文も作成する。
|
||||
* @context Context
|
||||
* @param issuedAccountId
|
||||
* @param toAccountId
|
||||
* @param nowDate
|
||||
*/
|
||||
async issueTrialLicense(
|
||||
context: Context,
|
||||
issuedAccountId: number,
|
||||
toAccountId: number,
|
||||
nowDate: Date,
|
||||
): Promise<void> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// 注文のレコードを作成する
|
||||
const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
|
||||
const trialLicenseOrder = new LicenseOrder();
|
||||
// PONumberは設定しないので初期値を使用する
|
||||
trialLicenseOrder.from_account_id = issuedAccountId;
|
||||
trialLicenseOrder.to_account_id = toAccountId;
|
||||
trialLicenseOrder.quantity = ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY;
|
||||
trialLicenseOrder.status = LICENSE_ISSUE_STATUS.ISSUED;
|
||||
trialLicenseOrder.ordered_at = nowDate;
|
||||
trialLicenseOrder.issued_at = nowDate;
|
||||
trialLicenseOrder.type = LICENSE_TYPE.TRIAL;
|
||||
await insertEntity(
|
||||
LicenseOrder,
|
||||
licenseOrderRepo,
|
||||
trialLicenseOrder,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// トライアルライセンスを発行する。
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
// トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
|
||||
const expiryDate = new NewTrialLicenseExpirationDate(nowDate);
|
||||
// ライセンステーブルのレコードを作成する
|
||||
const newTrialLicenses = Array.from(
|
||||
{ length: trialLicenseOrder.quantity },
|
||||
() => {
|
||||
const license = new License();
|
||||
license.expiry_date = expiryDate;
|
||||
license.account_id = issuedAccountId;
|
||||
license.type = LICENSE_TYPE.TRIAL;
|
||||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||||
license.order_id = trialLicenseOrder.id;
|
||||
return license;
|
||||
},
|
||||
);
|
||||
|
||||
// ライセンステーブルを登録(注文元)
|
||||
await insertEntities(
|
||||
License,
|
||||
licenseRepo,
|
||||
newTrialLicenses,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 対象のアカウントの割り当て可能なライセンスを取得する
|
||||
* @context Context
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'task_filters' })
|
||||
export class TaskFilters {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
author_id: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
file_name: string | null;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TaskFiltersRepositoryService } from './task_filter.repository.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TaskFilters } from './entity/task_filters.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TaskFilters])],
|
||||
providers: [TaskFiltersRepositoryService],
|
||||
exports: [TaskFiltersRepositoryService],
|
||||
})
|
||||
export class TaskFiltersRepositoryModule {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user