Compare commits
29 Commits
release-20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d9a254b63 | ||
|
|
ff5533b647 | ||
|
|
b3845187f6 | ||
|
|
019c818a19 | ||
|
|
1137d826ae | ||
|
|
b529388871 | ||
|
|
aef17893d9 | ||
|
|
4f598b0017 | ||
|
|
b71ec627d7 | ||
|
|
af56f8ccad | ||
|
|
0b451ed62f | ||
|
|
11395279af | ||
|
|
a07cfe51aa | ||
|
|
ad397f6fe7 | ||
|
|
85fdec2e5a | ||
|
|
f1b75a7ff0 | ||
|
|
6690302ac3 | ||
|
|
1320222d79 | ||
|
|
baea8ce5e5 | ||
|
|
d69126a980 | ||
|
|
79a2b9f0a3 | ||
|
|
2a755f2bd3 | ||
|
|
edb8a79f94 | ||
|
|
4a5136deee | ||
|
|
f2eaba7e5f | ||
|
|
3b576c6a47 | ||
|
|
557fc48d05 | ||
|
|
353d5ad462 | ||
|
|
df55be5e19 |
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
|
||||
@ -43,11 +43,14 @@ jobs:
|
||||
targetType: inline
|
||||
workingDirectory: dictation_server/.devcontainer
|
||||
script: |
|
||||
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
|
||||
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')
|
||||
@ -186,6 +189,9 @@ jobs:
|
||||
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
|
||||
|
||||
@ -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",
|
||||
{
|
||||
|
||||
@ -11,6 +11,13 @@ import { selectSnackber } from "features/ui/selectors";
|
||||
import { closeSnackbar } from "features/ui/uiSlice";
|
||||
import { UNAUTHORIZED_TO_CONTINUE_ERROR_CODES } from "components/auth/constants";
|
||||
import { clearUserInfo } from "features/login";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
|
||||
/*
|
||||
UpdateTokenTimerをApp.tsxに移動する(2024年6月27日)
|
||||
各画面ごとにトークンの期限チェック~自動更新を行う処理を配置していたが、
|
||||
全画面で共通の処理であることと、画面遷移時にチェックのインターバルがリセットされることを考慮し、App.tsxに移動する。
|
||||
*/
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
@ -82,6 +89,7 @@ const App = (): JSX.Element => {
|
||||
/>
|
||||
<BrowserRouter>
|
||||
<AppRouter />
|
||||
<UpdateTokenTimer />
|
||||
</BrowserRouter>
|
||||
</>
|
||||
);
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useLayoutEffect, useState } from "react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { decodeToken } from "common/decodeToken";
|
||||
import { useInterval } from "common/useInterval";
|
||||
@ -17,41 +17,58 @@ import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants";
|
||||
export const UpdateTokenTimer = () => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
// トークンの更新中かどうか
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const delegattionToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
// 期限が5分以内であれば更新APIを呼ぶ
|
||||
const updateToken = useCallback(async () => {
|
||||
// localStorageからトークンを取得
|
||||
const jwt = loadAccessToken();
|
||||
// 現在時刻を取得
|
||||
const now = DateTime.local().toSeconds();
|
||||
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
|
||||
if (jwt) {
|
||||
const token = decodeToken(jwt);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
await dispatch(updateTokenAsync());
|
||||
}
|
||||
}
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 代行操作トークン更新処理
|
||||
if (delegattionToken) {
|
||||
const token = decodeToken(delegattionToken);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
const { meta } = await dispatch(updateDelegationTokenAsync());
|
||||
if (meta.requestStatus === "rejected") {
|
||||
dispatch(cleanupDelegateAccount());
|
||||
navigate("/partners");
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
// localStorageからトークンを取得
|
||||
const jwt = loadAccessToken();
|
||||
// 現在時刻を取得
|
||||
const now = DateTime.local().toSeconds();
|
||||
// selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
|
||||
if (jwt) {
|
||||
const token = decodeToken(jwt);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
await dispatch(updateTokenAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 代行操作トークン更新処理
|
||||
if (delegattionToken) {
|
||||
const token = decodeToken(delegattionToken);
|
||||
if (token) {
|
||||
const { exp } = token;
|
||||
if (exp - now <= TOKEN_UPDATE_TIME) {
|
||||
const { meta } = await dispatch(updateDelegationTokenAsync());
|
||||
if (meta.requestStatus === "rejected") {
|
||||
dispatch(cleanupDelegateAccount());
|
||||
navigate("/partners");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Token update error:", e);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [dispatch, delegattionToken, navigate]);
|
||||
}, [isUpdating, delegattionToken, dispatch, navigate]);
|
||||
useLayoutEffect(() => {
|
||||
updateToken();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS);
|
||||
|
||||
|
||||
@ -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<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { AppDispatch } from "app/store";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
@ -106,7 +105,6 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
|
||||
@ -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"))}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import { isApproveTier } from "features/auth";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import Footer from "components/footer";
|
||||
@ -13,6 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
LIMIT_ORDER_HISORY_NUM,
|
||||
STATUS,
|
||||
LICENSE_TYPE,
|
||||
getLicenseOrderHistoriesAsync,
|
||||
selectCurrentPage,
|
||||
selectIsLoading,
|
||||
@ -26,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);
|
||||
@ -47,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);
|
||||
|
||||
@ -65,6 +65,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
selectedRow,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -152,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 : ""}`}
|
||||
@ -166,7 +171,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
delegationAccessToken && <DelegationBar />
|
||||
}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
@ -210,6 +214,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getTranslationID("orderHistoriesPage.label.issueDate")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("orderHistoriesPage.label.licenseType")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID(
|
||||
@ -231,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) {
|
||||
@ -261,7 +271,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
})()}
|
||||
</td>
|
||||
<td>
|
||||
{!selectedRow && (
|
||||
{!selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
@ -286,7 +296,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow && (
|
||||
{selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import styles from "styles/app.module.scss";
|
||||
@ -15,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";
|
||||
@ -28,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);
|
||||
|
||||
@ -50,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);
|
||||
@ -59,6 +61,10 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
setIsCardLicenseActivatePopupOpen(true);
|
||||
}, [setIsCardLicenseActivatePopupOpen]);
|
||||
|
||||
const onTrialLicenseIssueOpen = useCallback(() => {
|
||||
setIsTrialLicenseIssuePopupOpen(true);
|
||||
}, [setIsTrialLicenseIssuePopupOpen]);
|
||||
|
||||
// 呼び出し画面制御関係
|
||||
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
@ -72,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(() => {
|
||||
@ -134,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 && (
|
||||
@ -149,8 +166,6 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
@ -231,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,44 +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 { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
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();
|
||||
@ -48,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);
|
||||
|
||||
// 階層表示用
|
||||
@ -85,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);
|
||||
@ -137,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ボタン押下時
|
||||
@ -156,6 +182,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
setIsChangeOwnerPopupOpen(true);
|
||||
}, [setIsChangeOwnerPopupOpen]);
|
||||
|
||||
const onOpenSearchPopup = useCallback(() => {
|
||||
dispatch(setIsSearchPopupOpen({ value: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
// マウント時のみ実行
|
||||
useEffect(() => {
|
||||
dispatch(getMyAccountAsync());
|
||||
@ -177,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(() => {
|
||||
@ -195,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]);
|
||||
|
||||
@ -222,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を生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
||||
@ -239,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 && (
|
||||
@ -260,10 +310,14 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
||||
{isVisiblePartnerSearch() && isSearchPopupOpen && (
|
||||
<SearchPartnerPopup
|
||||
onClose={() => dispatch(setIsSearchPopupOpen({ value: true }))}
|
||||
/>
|
||||
)}
|
||||
{isVisiblePartnerLicensePage && (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
@ -364,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) => (
|
||||
@ -390,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")
|
||||
@ -415,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
|
||||
: ""
|
||||
}
|
||||
@ -458,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
|
||||
@ -474,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>
|
||||
@ -515,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>
|
||||
@ -526,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>
|
||||
@ -553,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>
|
||||
@ -586,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>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import { AppDispatch } from "app/store";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
@ -169,7 +168,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
/>
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import Header from "components/header";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import styles from "styles/app.module.scss";
|
||||
@ -8,11 +7,18 @@ import Footer from "components/footer";
|
||||
|
||||
const SupportPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
// OMDS_IS-381 Support画面で表示する内容を充実したいの対応 2024年8月7日
|
||||
const userGuideDivStyles: React.CSSProperties = {
|
||||
padding: "2rem 2rem 4rem 2rem",
|
||||
};
|
||||
const appDLDivStyles: React.CSSProperties = {
|
||||
padding: "2rem 2rem 0rem 2rem",
|
||||
};
|
||||
const customUlStyles: React.CSSProperties = { marginBottom: "1rem" };
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
@ -25,8 +31,8 @@ const SupportPage: React.FC = () => {
|
||||
<div>
|
||||
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
|
||||
|
||||
<div className={styles.txContents}>
|
||||
<ul className={styles.listDocument}>
|
||||
<div className={styles.txContents} style={userGuideDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/manual/odms_cloud/"
|
||||
@ -42,6 +48,105 @@ const SupportPage: React.FC = () => {
|
||||
{t(getTranslationID("supportPage.text.notResolved"))}
|
||||
</p>
|
||||
</div>
|
||||
<h2>
|
||||
{t(getTranslationID("supportPage.label.programDownload"))}
|
||||
</h2>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/odms_cloud_desktop/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.omdsDesktopAppDownloadLink"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.omdsDesktopAppDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/device_customization_program/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(getTranslationID("supportPage.label.dcpDownloadLink"))}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.dcpDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/odms_cloud_backup_extraction_tool/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.backupExtractionToolDownloadLink"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.backupExtractionToolDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.txContents} style={appDLDivStyles}>
|
||||
<ul className={styles.listDocument} style={customUlStyles}>
|
||||
<li>
|
||||
<a
|
||||
href="https://download.omsystem.com/pages/odms_download/odms_client_virtual_driver/en/"
|
||||
target="_blank"
|
||||
className={styles.linkTx}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.virtualDriverDownloadLink"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className={styles.txNormal}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"supportPage.label.virtualDriverDownloadLinkDescription"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "app/store";
|
||||
import Header from "components/header";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import undo from "assets/images/undo.svg";
|
||||
@ -69,7 +68,6 @@ export const TemplateFilePage: React.FC = () => {
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import progress_activit from "assets/images/progress_activit.svg";
|
||||
import undo from "assets/images/undo.svg";
|
||||
import group_add from "assets/images/group_add.svg";
|
||||
@ -93,7 +92,6 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
listUsersAsync,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
selectIsLoading,
|
||||
deallocateLicenseAsync,
|
||||
deleteUserAsync,
|
||||
confirmUserForceAsync,
|
||||
} from "features/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
@ -33,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";
|
||||
@ -49,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);
|
||||
@ -85,6 +88,7 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
@ -103,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());
|
||||
@ -127,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}
|
||||
@ -153,7 +201,6 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
@ -187,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}`}>
|
||||
@ -310,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());
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
@ -138,7 +137,6 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<div className={styles.pageHeader}>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import ruleAddImg from "assets/images/rule_add.svg";
|
||||
import templateSettingImg from "assets/images/template_setting.svg";
|
||||
import worktypeSettingImg from "assets/images/worktype_setting.svg";
|
||||
@ -83,7 +82,6 @@ const WorkflowPage: React.FC = (): JSX.Element => {
|
||||
>
|
||||
{delegationAccessToken && <DelegationBar />}
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
|
||||
@ -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.",
|
||||
@ -617,7 +628,16 @@
|
||||
"label": {
|
||||
"title": "Support",
|
||||
"howToUse": "So verwenden Sie das System",
|
||||
"supportPageLink": "OMDS Cloud-Benutzerhandbuch"
|
||||
"supportPageLink": "OMDS Cloud-Benutzerhandbuch",
|
||||
"programDownload": "Programm-Download",
|
||||
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
|
||||
"omdsDesktopAppDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Cloud Desktop App zu gelangen.",
|
||||
"dcpDownloadLink": "Device Configuration Program (DCP)",
|
||||
"dcpDownloadLinkDescription": "Klicken Sie hier, um zur DCP-Downloadseite zu gelangen.",
|
||||
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
|
||||
"backupExtractionToolDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Cloud Backup Extraction Tool zu gelangen.",
|
||||
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
|
||||
"virtualDriverDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Client Virtual Driver zu gelangen."
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "Informationen zu den Funktionen der ODMS Cloud finden Sie im Benutzerhandbuch. Wenn Sie zusätzlichen Support benötigen, wenden Sie sich bitte an Ihren Administrator oder zertifizierten ODMS Cloud-Händler."
|
||||
@ -653,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"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.",
|
||||
@ -617,7 +628,16 @@
|
||||
"label": {
|
||||
"title": "Support",
|
||||
"howToUse": "How to use the system",
|
||||
"supportPageLink": "OMDS Cloud User Guide"
|
||||
"supportPageLink": "OMDS Cloud User Guide",
|
||||
"programDownload": "Program Download",
|
||||
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
|
||||
"omdsDesktopAppDownloadLinkDescription": "Click here to go to the ODMS Cloud Desktop App download page.",
|
||||
"dcpDownloadLink": "Device Configuration Program (DCP)",
|
||||
"dcpDownloadLinkDescription": "Click here to go to the DCP download page.",
|
||||
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
|
||||
"backupExtractionToolDownloadLinkDescription": "Click here to go to the ODMS Cloud Backup Extraction Tool download page.",
|
||||
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
|
||||
"virtualDriverDownloadLinkDescription": "Click here to go to the ODMS Client Virtual Driver download page."
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "Please refer to the User Guide for information about the features of the ODMS Cloud. If you require additional support, please contact your administrator or certified ODMS Cloud reseller."
|
||||
@ -653,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"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.",
|
||||
@ -617,7 +628,16 @@
|
||||
"label": {
|
||||
"title": "Soporte",
|
||||
"howToUse": "Cómo utilizar el sistema",
|
||||
"supportPageLink": "Guía del usuario de la nube OMDS"
|
||||
"supportPageLink": "Guía del usuario de la nube OMDS",
|
||||
"programDownload": "Descarga del programa",
|
||||
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
|
||||
"omdsDesktopAppDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Cloud Desktop App.",
|
||||
"dcpDownloadLink": "Device Configuration Program (DCP)",
|
||||
"dcpDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de DCP.",
|
||||
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
|
||||
"backupExtractionToolDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Cloud Backup Extraction Tool.",
|
||||
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
|
||||
"virtualDriverDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Client Virtual Driver."
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "Consulte la Guía del usuario para obtener información sobre las funciones de ODMS Cloud. Si necesita soporte adicional, comuníquese con su administrador o revendedor certificado de ODMS Cloud."
|
||||
@ -653,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"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.",
|
||||
@ -617,7 +628,16 @@
|
||||
"label": {
|
||||
"title": "Support",
|
||||
"howToUse": "Comment utiliser le système",
|
||||
"supportPageLink": "Guide de l'utilisateur du cloud OMDS"
|
||||
"supportPageLink": "Guide de l'utilisateur du cloud OMDS",
|
||||
"programDownload": "Télécharger le programme",
|
||||
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
|
||||
"omdsDesktopAppDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Cloud Desktop App.",
|
||||
"dcpDownloadLink": "Device Configuration Program (DCP)",
|
||||
"dcpDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement du DCP.",
|
||||
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
|
||||
"backupExtractionToolDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Cloud Backup Extraction Tool.",
|
||||
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
|
||||
"virtualDriverDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Client Virtual Driver."
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "Veuillez vous référer au Guide de l'utilisateur pour plus d'informations sur les fonctionnalités d'ODMS Cloud. Si vous avez besoin d'une assistance supplémentaire, veuillez contacter votre administrateur ou votre revendeur certifié ODMS Cloud."
|
||||
@ -653,5 +673,22 @@
|
||||
"upperLayerId": "Upper Layer ID",
|
||||
"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
|
||||
|
||||
@ -106,6 +106,7 @@ export const LICENSE_TYPE = {
|
||||
TRIAL: "TRIAL",
|
||||
NORMAL: "NORMAL",
|
||||
CARD: "CARD",
|
||||
NONE: "NONE",
|
||||
} as const;
|
||||
/**
|
||||
* ライセンス状態
|
||||
@ -350,6 +351,7 @@ export const FILE_RETENTION_DAYS_DEFAULT = 30;
|
||||
*/
|
||||
export const LICENSE_COUNT_ANALYSIS_HEADER = {
|
||||
ACCOUNT: "アカウント",
|
||||
COUNTRY:"国",
|
||||
TARGET_YEAE_AND_MONTH: "対象年月",
|
||||
CATEGORY_1: "カテゴリー1",
|
||||
CATEGORY_2: "カテゴリー2",
|
||||
@ -385,6 +387,7 @@ export const LICENSE_COUNT_ANALYSIS_LICENSE_TYPE = {
|
||||
CARD: "Card",
|
||||
SWITCH_FROM_TRIAL: "トライアルから切り替え",
|
||||
SWITCH_FROM_CARD: "カードから切り替え",
|
||||
NONE: "移行ライセンス",
|
||||
};
|
||||
/**
|
||||
* ライセンス数推移出力機能のCSV項目で使用する日本語(役割)
|
||||
|
||||
@ -435,6 +435,7 @@ export async function transferData(
|
||||
// 出力データのヘッダーを作成
|
||||
const header = [
|
||||
'"' + LICENSE_COUNT_ANALYSIS_HEADER.ACCOUNT + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_HEADER.COUNTRY + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_HEADER.TARGET_YEAE_AND_MONTH + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_1 + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_2 + '",',
|
||||
@ -486,6 +487,7 @@ export async function transferData(
|
||||
(license) => license.account_id === account.id
|
||||
);
|
||||
// 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード)
|
||||
// 種別にNoneを追加⇒旧システムからの移行分の状況も確認したい 2024年7月9日
|
||||
const trialLicenses = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.TRIAL
|
||||
);
|
||||
@ -495,6 +497,9 @@ export async function transferData(
|
||||
const cardLicenses = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.CARD
|
||||
);
|
||||
const noneLicense = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.NONE
|
||||
);
|
||||
// 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated
|
||||
const usedTrialLicenses = trialLicenses.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
@ -505,6 +510,9 @@ export async function transferData(
|
||||
const usedCardLicenses = cardLicenses.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
);
|
||||
const usedNoneLicense = noneLicense.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
);
|
||||
// どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。
|
||||
// (allcated_user_idからユーザーを特定)
|
||||
// (Author・Typist・None)
|
||||
@ -584,6 +592,7 @@ export async function transferData(
|
||||
const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length;
|
||||
const usedCardLicensesTypistCount = usedCardLicensesTypist.length;
|
||||
const usedCardLicensesNoneCount = usedCardLicensesNone.length;
|
||||
const usedNoneLicenseCount = usedNoneLicense.length;
|
||||
|
||||
// アカウントに紐づく当月発行ライセンスを取得
|
||||
const accountCurrentMonthIssuedLicenses =
|
||||
@ -813,10 +822,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.company_name,
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -854,10 +865,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.company_name,
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -895,10 +908,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.company_name,
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -959,6 +974,7 @@ export async function transferData(
|
||||
(license) => license.account_id === account.id
|
||||
);
|
||||
// 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード)
|
||||
// 種別にNoneを追加⇒旧システムからの移行分の状況も確認したい 2024年7月9日
|
||||
const trialLicenses = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.TRIAL
|
||||
);
|
||||
@ -968,6 +984,9 @@ export async function transferData(
|
||||
const cardLicenses = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.CARD
|
||||
);
|
||||
const noneLicense = accountLicenses.filter(
|
||||
(license) => license.type === LICENSE_TYPE.NONE
|
||||
);
|
||||
// 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated
|
||||
const usedTrialLicenses = trialLicenses.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
@ -978,6 +997,9 @@ export async function transferData(
|
||||
const usedCardLicenses = cardLicenses.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
);
|
||||
const usedNoneLicense = noneLicense.filter(
|
||||
(license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
);
|
||||
// どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。
|
||||
// (allcated_user_idからユーザーを特定)
|
||||
// (Author・Typist・None)
|
||||
@ -1048,6 +1070,7 @@ export async function transferData(
|
||||
const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length;
|
||||
const usedCardLicensesTypistCount = usedCardLicensesTypist.length;
|
||||
const usedCardLicensesNoneCount = usedCardLicensesNone.length;
|
||||
const usedNoneLicenseCount = usedNoneLicense.length;
|
||||
|
||||
// アカウントに紐づく当月発行ライセンスを取得
|
||||
const accountCurrentMonthIssuedLicenses =
|
||||
@ -1252,10 +1275,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.id.toString(),
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -1293,10 +1318,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.id.toString(),
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -1334,10 +1361,12 @@ export async function transferData(
|
||||
await createOutputData(
|
||||
context,
|
||||
account.id.toString(),
|
||||
account.country,
|
||||
targetMonthYYYYMM,
|
||||
trialLicensesCount,
|
||||
normalLicensesCount,
|
||||
cardLicensesCount,
|
||||
usedNoneLicenseCount,
|
||||
usedTrialLicensesAuthorCount,
|
||||
usedTrialLicensesTypistCount,
|
||||
usedTrialLicensesNoneCount,
|
||||
@ -1429,10 +1458,12 @@ export async function transferData(
|
||||
async function createOutputData(
|
||||
context: InvocationContext,
|
||||
company_name: string,
|
||||
country: string,
|
||||
targetMonthYYYYMM: string,
|
||||
trialLicensesCount: number,
|
||||
normalLicensesCount: number,
|
||||
cardLicensesCount: number,
|
||||
usedNoneLicenseCount: number,
|
||||
usedTrialLicensesAuthorCount: number,
|
||||
usedTrialLicensesTypistCount: number,
|
||||
usedTrialLicensesNoneCount: number,
|
||||
@ -1471,6 +1502,8 @@ async function createOutputData(
|
||||
resultOutputData.push(
|
||||
// 会社名(ダブルクォーテーションで囲む)
|
||||
'"' + company_name + '",',
|
||||
// 国
|
||||
'"' + country + '",',
|
||||
// 対象年月(先月)YYYYMM
|
||||
// 2024年3月に実行した場合:202402
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
@ -1488,6 +1521,7 @@ async function createOutputData(
|
||||
// アカウントが保持する通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",',
|
||||
@ -1498,6 +1532,7 @@ async function createOutputData(
|
||||
// アカウントが保持するカードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",',
|
||||
@ -1505,9 +1540,21 @@ async function createOutputData(
|
||||
'"' + "" + '",',
|
||||
'"' + cardLicensesCount.toString() + '"\r\n'
|
||||
);
|
||||
// ユーザーが使用中の移行ライセンス
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.NONE + '",',
|
||||
'"' + "" + '",',
|
||||
'"' + usedNoneLicenseCount.toString() + '"\r\n'
|
||||
);
|
||||
// Authorが使用中のトライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1518,6 +1565,7 @@ async function createOutputData(
|
||||
// Typistが使用中のトライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1528,6 +1576,7 @@ async function createOutputData(
|
||||
// Noneが使用中のトライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1538,6 +1587,7 @@ async function createOutputData(
|
||||
// Authorが使用中の通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1548,6 +1598,7 @@ async function createOutputData(
|
||||
// Typistが使用中の通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1558,6 +1609,7 @@ async function createOutputData(
|
||||
// Noneが使用中の通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1568,6 +1620,7 @@ async function createOutputData(
|
||||
// Authorが使用中のカードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1578,6 +1631,7 @@ async function createOutputData(
|
||||
// Typistが使用中のカードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1588,6 +1642,7 @@ async function createOutputData(
|
||||
// Noneが使用中のカードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",',
|
||||
@ -1598,6 +1653,7 @@ async function createOutputData(
|
||||
// アカウントが保持する当月発行トライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1608,6 +1664,7 @@ async function createOutputData(
|
||||
// アカウントが保持する当月発行通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1618,6 +1675,7 @@ async function createOutputData(
|
||||
// アカウントが保持する当月発行カードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1628,6 +1686,7 @@ async function createOutputData(
|
||||
// Authorに割り当てられたままの失効トライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1638,6 +1697,7 @@ async function createOutputData(
|
||||
// Typistに割り当てられたままの失効トライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1648,6 +1708,7 @@ async function createOutputData(
|
||||
// Noneに割り当てられたままの失効トライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1658,6 +1719,7 @@ async function createOutputData(
|
||||
// 未割当の失効トライアルライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1668,6 +1730,7 @@ async function createOutputData(
|
||||
// Authorに割り当てられたままの失効通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1678,6 +1741,7 @@ async function createOutputData(
|
||||
// Typistに割り当てられたままの失効通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1688,6 +1752,7 @@ async function createOutputData(
|
||||
// Noneに割り当てられたままの失効通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1698,6 +1763,7 @@ async function createOutputData(
|
||||
// 未割当の失効通常ライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1708,6 +1774,7 @@ async function createOutputData(
|
||||
// Authorに割り当てられたままの失効カードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1718,6 +1785,7 @@ async function createOutputData(
|
||||
// Typistに割り当てられたままの失効カードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1728,6 +1796,7 @@ async function createOutputData(
|
||||
// Noneに割り当てられたままの失効カードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1738,6 +1807,7 @@ async function createOutputData(
|
||||
// 未割当の失効カードライセンス[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1748,6 +1818,7 @@ async function createOutputData(
|
||||
// Authorにトライアルライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1758,6 +1829,7 @@ async function createOutputData(
|
||||
// Typistにトライアルライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1768,6 +1840,7 @@ async function createOutputData(
|
||||
// Noneにトライアルライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1778,6 +1851,7 @@ async function createOutputData(
|
||||
// Authorにカードライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1788,6 +1862,7 @@ async function createOutputData(
|
||||
// Typistにカードライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
@ -1798,6 +1873,7 @@ async function createOutputData(
|
||||
// Noneにカードライセンスからの切り替え[]
|
||||
resultOutputData.push(
|
||||
'"' + company_name + '",',
|
||||
'"' + country + '",',
|
||||
'"' + targetMonthYYYYMM + '",',
|
||||
'"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",',
|
||||
'"' + "" + '",',
|
||||
|
||||
@ -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,25 +655,26 @@ 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 ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
|
||||
const uniqueCustomerAdminMails = [...new Set(customerAdminMails)];
|
||||
const ccMails = uniqueCustomerAdminMails.includes(userMail) ? [] : [userMail];
|
||||
|
||||
// メールを送信する
|
||||
await sendGrid.sendMail(
|
||||
context,
|
||||
customerAdminMails,
|
||||
ccAddress,
|
||||
uniqueCustomerAdminMails,
|
||||
ccMails,
|
||||
mailFrom,
|
||||
subject,
|
||||
text,
|
||||
@ -693,3 +694,10 @@ class autoAllocationList {
|
||||
accountId: number;
|
||||
userIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与えられた文字列内の $ を $$ にエスケープする
|
||||
* @param str - エスケープする対象の文字列
|
||||
* @returns エスケープ後の文字列
|
||||
*/
|
||||
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");
|
||||
133
dictation_function/src/sendgrid/sendgrid.spec.ts
Normal file
133
dictation_function/src/sendgrid/sendgrid.spec.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { SendGridService } from "./sendgrid";
|
||||
import sendgrid from "@sendgrid/mail";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
// sendgridのsend関数をモック化
|
||||
jest.mock("@sendgrid/mail", () => ({
|
||||
send: jest.fn(),
|
||||
setApiKey: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("SendGridService", () => {
|
||||
let service: SendGridService;
|
||||
let mockContext: Partial<InvocationContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.SENDGRID_API_KEY = "dummy_key"; // 必要な環境変数
|
||||
service = new SendGridService(); // SendGridServiceのインスタンスを作成
|
||||
mockContext = {
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}; // InvocationContextのモック
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should send an email with no duplicate to and cc addresses", async () => {
|
||||
// モックデータ
|
||||
const to = ["test1@example.com", "test2@example.com"];
|
||||
const cc = ["test3@example.com"];
|
||||
const from = "sender@example.com";
|
||||
const subject = "Test Subject";
|
||||
const text = "Test Text";
|
||||
const html = "<p>Test HTML</p>";
|
||||
|
||||
// sendgrid.sendのモック
|
||||
(sendgrid.send as jest.Mock).mockResolvedValue([
|
||||
{ statusCode: 202, body: "OK" },
|
||||
]);
|
||||
|
||||
// メール送信を実行
|
||||
await service.sendMail(
|
||||
mockContext as InvocationContext,
|
||||
to,
|
||||
cc,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
);
|
||||
|
||||
// sendgrid.sendが呼ばれたことを確認
|
||||
expect(sendgrid.send).toHaveBeenCalledWith({
|
||||
from: { email: from },
|
||||
to: to.map((v) => ({ email: v })),
|
||||
cc: cc.map((v) => ({ email: v })),
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
// ログが出力されているか確認
|
||||
expect(mockContext.log).toHaveBeenCalledWith(`[IN] sendMail`);
|
||||
expect(mockContext.log).toHaveBeenCalledWith(`[OUT] sendMail`);
|
||||
});
|
||||
|
||||
it("should remove duplicate addresses in to and cc", async () => {
|
||||
const to = ["test1@example.com", "test2@example.com", "test1@example.com"]; // 重複あり
|
||||
const cc = ["test2@example.com", "test3@example.com"]; // 重複あり
|
||||
const from = "sender@example.com";
|
||||
const subject = "Test Subject";
|
||||
const text = "Test Text";
|
||||
const html = "<p>Test HTML</p>";
|
||||
|
||||
// sendgrid.sendのモック
|
||||
(sendgrid.send as jest.Mock).mockResolvedValue([
|
||||
{ statusCode: 202, body: "OK" },
|
||||
]);
|
||||
|
||||
// メール送信を実行
|
||||
await service.sendMail(
|
||||
mockContext as InvocationContext,
|
||||
to,
|
||||
cc,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
);
|
||||
|
||||
// 重複が削除されているか確認
|
||||
expect(sendgrid.send).toHaveBeenCalledWith({
|
||||
from: { email: from },
|
||||
to: [{ email: "test1@example.com" }, { email: "test2@example.com" }], // 重複削除後
|
||||
cc: [{ email: "test3@example.com" }], // ccから重複が削除される
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
});
|
||||
|
||||
it("should log an error when send fails", async () => {
|
||||
const to = ["test1@example.com"];
|
||||
const cc = ["test2@example.com"];
|
||||
const from = "sender@example.com";
|
||||
const subject = "Test Subject";
|
||||
const text = "Test Text";
|
||||
const html = "<p>Test HTML</p>";
|
||||
|
||||
// sendgrid.sendがエラーを投げるモック
|
||||
(sendgrid.send as jest.Mock).mockRejectedValue(new Error("Send failed"));
|
||||
|
||||
// エラーが投げられるか確認
|
||||
await expect(
|
||||
service.sendMail(
|
||||
mockContext as InvocationContext,
|
||||
to,
|
||||
cc,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
)
|
||||
).rejects.toThrow("Send failed");
|
||||
|
||||
// エラーログが出力されているか確認
|
||||
expect(mockContext.warn).toHaveBeenCalledWith("send mail faild.");
|
||||
expect(mockContext.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("sendMail error=Error: Send failed")
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -32,22 +32,28 @@ export class SendGridService {
|
||||
): Promise<void> {
|
||||
context.log(`[IN] ${this.sendMail.name}`);
|
||||
try {
|
||||
// 1. toの重複を削除
|
||||
const uniqueTo = [...new Set(to)];
|
||||
|
||||
// 2. ccの重複を削除
|
||||
let uniqueCc = [...new Set(cc)];
|
||||
|
||||
// 3. toとccの重複を削除(cc側から削除)
|
||||
uniqueCc = uniqueCc.filter((email) => !uniqueTo.includes(email));
|
||||
const res = await sendgrid
|
||||
.send({
|
||||
from: {
|
||||
email: from,
|
||||
},
|
||||
to: to.map((v) => ({ email: v })),
|
||||
cc: cc.map((v) => ({ email: v })),
|
||||
to: uniqueTo.map((v) => ({ email: v })),
|
||||
cc: uniqueCc.map((v) => ({ email: v })),
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
})
|
||||
.then((v) => v[0]);
|
||||
context.log(
|
||||
` status code: ${
|
||||
res.statusCode
|
||||
} body: ${JSON.stringify(res.body)}`,
|
||||
` status code: ${res.statusCode} body: ${JSON.stringify(res.body)}`
|
||||
);
|
||||
} catch (e) {
|
||||
context.warn(`send mail faild.`);
|
||||
|
||||
@ -23,10 +23,6 @@ import {
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
import {
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_COUNT_ANALYSIS_CATEGORY_1,
|
||||
LICENSE_COUNT_ANALYSIS_CATEGORY_2,
|
||||
LICENSE_COUNT_ANALYSIS_LICENSE_TYPE,
|
||||
LICENSE_COUNT_ANALYSIS_ROLE,
|
||||
SWITCH_FROM_TYPE,
|
||||
} from "../constants";
|
||||
import { BlobstorageService } from "../blobstorage/blobstorage.service";
|
||||
@ -305,6 +301,35 @@ describe("analysisLicenses", () => {
|
||||
last2Month
|
||||
);
|
||||
|
||||
// 有効な移行ライセンスの作成
|
||||
await createLicense(
|
||||
source,
|
||||
26,
|
||||
expiringSoonDate,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
// 期限切れの移行ライセンスの作成
|
||||
await createLicense(
|
||||
source,
|
||||
27,
|
||||
lastMonth,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
|
||||
// 第五階層がその月におこなったライセンス切り替え情報を作成
|
||||
// 条件:
|
||||
// ・第五アカウント
|
||||
@ -379,23 +404,25 @@ describe("analysisLicenses", () => {
|
||||
throw new Error("ユーザー取得できていないので失敗");
|
||||
}
|
||||
|
||||
expect(result.avairableLicenses).toHaveLength(6);
|
||||
expect(result.avairableLicenses).toHaveLength(7);
|
||||
expect(result.avairableLicenses[0].id).toBe(1);
|
||||
expect(result.avairableLicenses[1].id).toBe(2);
|
||||
expect(result.avairableLicenses[2].id).toBe(3);
|
||||
expect(result.avairableLicenses[3].id).toBe(11);
|
||||
expect(result.avairableLicenses[4].id).toBe(12);
|
||||
expect(result.avairableLicenses[5].id).toBe(13);
|
||||
expect(result.avairableLicenses[6].id).toBe(26);
|
||||
|
||||
expect(result.licensesIssuedInTargetMonth).toHaveLength(3);
|
||||
expect(result.licensesIssuedInTargetMonth[0].id).toBe(11);
|
||||
expect(result.licensesIssuedInTargetMonth[1].id).toBe(12);
|
||||
expect(result.licensesIssuedInTargetMonth[2].id).toBe(13);
|
||||
|
||||
expect(result.licensesExpiredInTargetMonth).toHaveLength(3);
|
||||
expect(result.licensesExpiredInTargetMonth).toHaveLength(4);
|
||||
expect(result.licensesExpiredInTargetMonth[0].id).toBe(21);
|
||||
expect(result.licensesExpiredInTargetMonth[1].id).toBe(22);
|
||||
expect(result.licensesExpiredInTargetMonth[2].id).toBe(23);
|
||||
expect(result.licensesExpiredInTargetMonth[3].id).toBe(27);
|
||||
|
||||
expect(result.switchedlicensesInTargetMonth).toHaveLength(2);
|
||||
expect(result.switchedlicensesInTargetMonth[0].id).toBe(1);
|
||||
@ -647,6 +674,35 @@ describe("analysisLicenses", () => {
|
||||
last2Month
|
||||
);
|
||||
|
||||
// 有効な移行ライセンスの作成
|
||||
await createLicenseArchive(
|
||||
source,
|
||||
26,
|
||||
null,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
// 期限切れの移行ライセンスの作成
|
||||
await createLicenseArchive(
|
||||
source,
|
||||
27,
|
||||
lastMonth,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
|
||||
// 第五階層がその月におこなったライセンス切り替え情報を作成
|
||||
// 条件:
|
||||
// ・第五アカウント
|
||||
@ -722,23 +778,25 @@ describe("analysisLicenses", () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(result.deletedAvairableLicenses).toHaveLength(6);
|
||||
expect(result.deletedAvairableLicenses).toHaveLength(7);
|
||||
expect(result.deletedAvairableLicenses[0].id).toBe(1);
|
||||
expect(result.deletedAvairableLicenses[1].id).toBe(2);
|
||||
expect(result.deletedAvairableLicenses[2].id).toBe(3);
|
||||
expect(result.deletedAvairableLicenses[3].id).toBe(11);
|
||||
expect(result.deletedAvairableLicenses[4].id).toBe(12);
|
||||
expect(result.deletedAvairableLicenses[5].id).toBe(13);
|
||||
expect(result.deletedAvairableLicenses[6].id).toBe(26);
|
||||
|
||||
expect(result.deletedLicensesIssuedInTargetMonth).toHaveLength(3);
|
||||
expect(result.deletedLicensesIssuedInTargetMonth[0].id).toBe(11);
|
||||
expect(result.deletedLicensesIssuedInTargetMonth[1].id).toBe(12);
|
||||
expect(result.deletedLicensesIssuedInTargetMonth[2].id).toBe(13);
|
||||
|
||||
expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(3);
|
||||
expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(4);
|
||||
expect(result.deletedLicensesExpiredInTargetMonth[0].id).toBe(21);
|
||||
expect(result.deletedLicensesExpiredInTargetMonth[1].id).toBe(22);
|
||||
expect(result.deletedLicensesExpiredInTargetMonth[2].id).toBe(23);
|
||||
expect(result.deletedLicensesExpiredInTargetMonth[3].id).toBe(27);
|
||||
|
||||
expect(result.deletedSwitchedlicensesInTargetMonth).toHaveLength(2);
|
||||
expect(result.deletedSwitchedlicensesInTargetMonth[0].id).toBe(1);
|
||||
@ -1682,6 +1740,62 @@ describe("analysisLicenses", () => {
|
||||
SWITCH_FROM_TYPE.CARD
|
||||
);
|
||||
|
||||
// 移行ライセンス用のデータを作成
|
||||
// 移行ライセンスを持つ生きているアカウントのユーザーの情報を作成する
|
||||
const activeUserForTransitionData1 = await makeTestUser(source, {
|
||||
account_id: account5_1.id,
|
||||
role: "author",
|
||||
});
|
||||
const activeUserForTransitionData2 = await makeTestUser(source, {
|
||||
account_id: account5_1.id,
|
||||
role: "author",
|
||||
});
|
||||
// ユーザーに紐づく有効な移行ライセンスを作成
|
||||
await createLicense(
|
||||
source,
|
||||
56,
|
||||
expiringSoonDate,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
activeUserForTransitionData1.id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
// ユーザーに紐づく期限切れの移行ライセンスを作成
|
||||
// これは集計しないのでCSVには出力されない
|
||||
await createLicense(
|
||||
source,
|
||||
57,
|
||||
lastMonth,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
activeUserForTransitionData2.id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
|
||||
// 誰にも紐づかない移行ライセンスを作成
|
||||
// これは集計しないのでCSVには出力されない
|
||||
await createLicense(
|
||||
source,
|
||||
58,
|
||||
expiringSoonDate,
|
||||
account5_1.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
|
||||
const result = await getBaseData(context, lastMonthYYYYMM, source);
|
||||
|
||||
// 削除されたアカウントとユーザーの情報を作成する
|
||||
@ -1690,6 +1804,7 @@ describe("analysisLicenses", () => {
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: account4.id,
|
||||
country: "CA",
|
||||
},
|
||||
{
|
||||
external_id: "external_id_tier5admin1",
|
||||
@ -2585,6 +2700,62 @@ describe("analysisLicenses", () => {
|
||||
lastMonth,
|
||||
SWITCH_FROM_TYPE.CARD
|
||||
);
|
||||
|
||||
// 移行ライセンス用のデータを作成
|
||||
// 移行ライセンスを持つ削除アカウントのユーザーの情報を作成する
|
||||
const DeletedUserForTransitionData1 = await makeTestUserArchive(source, {
|
||||
account_id: account5_1_D.id,
|
||||
role: "author",
|
||||
});
|
||||
const DeletedUserForTransitionData2 = await makeTestUserArchive(source, {
|
||||
account_id: account5_1_D.id,
|
||||
role: "author",
|
||||
});
|
||||
// 移行ライセンスを生きているアカウントに作成
|
||||
await createLicenseArchive(
|
||||
source,
|
||||
56,
|
||||
expiringSoonDate,
|
||||
account5_1_D.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
DeletedUserForTransitionData1.id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
// ユーザーに紐づく期限切れの移行ライセンスを作成
|
||||
// これは集計しないのでCSVには出力されない
|
||||
await createLicenseArchive(
|
||||
source,
|
||||
57,
|
||||
lastMonth,
|
||||
account5_1_D.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
DeletedUserForTransitionData2.id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
// 誰にも紐づかない移行ライセンスを作成
|
||||
// これは集計しないのでCSVには出力されない
|
||||
await createLicenseArchive(
|
||||
source,
|
||||
58,
|
||||
expiringSoonDate,
|
||||
account5_1_D.id,
|
||||
"NONE",
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
last2Month
|
||||
);
|
||||
|
||||
const result_D = await getBaseDataFromDeletedAccounts(
|
||||
context,
|
||||
lastMonthYYYYMM,
|
||||
@ -2602,139 +2773,143 @@ describe("analysisLicenses", () => {
|
||||
csvContentUS += transferDataResult.outputDataUS[i];
|
||||
}
|
||||
expect(csvContentUS).toBe(
|
||||
'"アカウント","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' +
|
||||
'"アカウント","国","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
|
||||
`\r\n` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","移行ライセンス","","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
|
||||
`"test inc.","US","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","移行ライセンス","","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` +
|
||||
"\r\n" +
|
||||
`"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` +
|
||||
"\r\n" +
|
||||
`"1","CA","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` +
|
||||
"\r\n"
|
||||
);
|
||||
});
|
||||
|
||||
@ -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',
|
||||
},
|
||||
@ -494,132 +496,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId);
|
||||
});
|
||||
|
||||
it('タスク作成時に、自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
const { author_id: authorAuthorId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
// ルーティング先のタイピストのユーザー
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
author_id: undefined,
|
||||
});
|
||||
// API実行者のユーザー
|
||||
const { external_id: myExternalId, id: myUserId } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'my-author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'MY_AUTHOR_ID',
|
||||
},
|
||||
);
|
||||
|
||||
// ワークタイプを作成
|
||||
const { id: worktypeId, custom_worktype_id } = await createWorktype(
|
||||
source,
|
||||
accountId,
|
||||
'worktypeId',
|
||||
);
|
||||
|
||||
// テンプレートファイルを作成
|
||||
const { id: templateFileId } = await createTemplateFile(
|
||||
source,
|
||||
accountId,
|
||||
'templateFile',
|
||||
'http://blob/url/templateFile.zip',
|
||||
);
|
||||
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
myUserId, // API実行者のユーザーIDを設定
|
||||
worktypeId,
|
||||
templateFileId,
|
||||
);
|
||||
// ユーザーグループを作成
|
||||
const { userGroupId } = await createUserGroupAndMember(
|
||||
source,
|
||||
accountId,
|
||||
'userGroupName',
|
||||
typistUserId, // ルーティング先のタイピストのユーザーIDを設定
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(
|
||||
source,
|
||||
workflowId,
|
||||
undefined,
|
||||
userGroupId, // ルーティング先のユーザーグループIDを設定
|
||||
);
|
||||
|
||||
// 初期値のジョブナンバーでjob_numberテーブルを作成
|
||||
await createJobNumber(source, accountId, '00000000');
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const notificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
myExternalId, // API実行者のユーザーIDを設定
|
||||
'http://blob/url/file.zip',
|
||||
authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
custom_worktype_id,
|
||||
optionItemList,
|
||||
false,
|
||||
);
|
||||
expect(result.jobNumber).toEqual('00000001');
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
);
|
||||
// 作成したタスクを取得
|
||||
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
|
||||
// タスクのチェックアウト権限を取得
|
||||
const resultCheckoutPermission = await getCheckoutPermissions(
|
||||
source,
|
||||
resultTask?.id ?? 0,
|
||||
);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(resultCheckoutPermission.length).toEqual(1);
|
||||
expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId);
|
||||
});
|
||||
it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないものが入っていても自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => {
|
||||
it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないIDが入っていた場合自動ルーティングを行うことができない', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
@ -705,7 +582,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
makeContext('trackingId', 'requestId'),
|
||||
myExternalId, // API実行者のユーザーIDを設定
|
||||
'http://blob/url/file.zip',
|
||||
'XXXXXXXXXX', // 音声ファイルの情報には、録音者のAuthorIDが入る
|
||||
'XXXXXX', // 存在しないAuthorIDを指定
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
@ -720,17 +597,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
false,
|
||||
);
|
||||
expect(result.jobNumber).toEqual('00000001');
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
[`user_${typistUserId}`],
|
||||
{
|
||||
authorId: 'XXXXXXXXXX',
|
||||
filename: 'file',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
);
|
||||
// 通知処理が呼ばれていないことを確認
|
||||
expect(notificationHubService.notify).not.toBeCalled();
|
||||
// 作成したタスクを取得
|
||||
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
|
||||
// タスクのチェックアウト権限を取得
|
||||
@ -739,13 +607,12 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
resultTask?.id ?? 0,
|
||||
);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(resultCheckoutPermission.length).toEqual(1);
|
||||
expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId);
|
||||
expect(resultTask?.template_file_id).toBeNull();
|
||||
// 存在しないAuthorIDを指定してタスクを作成したためルーティングが行われず、タスクのチェックアウト権限は誰にも付与されない
|
||||
expect(resultCheckoutPermission.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('ワークフローが見つからない場合、タスク作成時に、自動ルーティングを行うことができない', async () => {
|
||||
it('ワークフローが見つからない場合、タスク作成時に自動ルーティングを行うことができない', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
@ -785,7 +652,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'worktypeId',
|
||||
'',
|
||||
optionItemList,
|
||||
false,
|
||||
);
|
||||
@ -802,6 +669,98 @@ describe('タスク作成から自動ルーティング(DB使用)', () => {
|
||||
// 自動ルーティングが行われていないことを確認
|
||||
expect(resultCheckoutPermission.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('WorkTypeIDの指定がないワークフローで、タスク作成時に自動ルーティングを行うことができる', async () => {
|
||||
if (!source) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// 音声ファイルの録音者のユーザー
|
||||
const {
|
||||
external_id: authorExternalId,
|
||||
author_id: authorAuthorId,
|
||||
id: authorUserId,
|
||||
} = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
author_id: undefined,
|
||||
});
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
undefined,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, typistUserId);
|
||||
|
||||
// 初期値のジョブナンバーでjob_numberテーブルを作成
|
||||
await createJobNumber(source, accountId, '00000000');
|
||||
|
||||
const blobParam = makeBlobstorageServiceMockValue();
|
||||
const notificationParam = makeDefaultNotificationhubServiceMockValue();
|
||||
|
||||
const module = await makeTestingModuleWithBlobAndNotification(
|
||||
source,
|
||||
blobParam,
|
||||
notificationParam,
|
||||
);
|
||||
if (!module) fail();
|
||||
const service = module.get<FilesService>(FilesService);
|
||||
const notificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
|
||||
const result = await service.uploadFinished(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
authorExternalId, // API実行者のユーザーIDを設定
|
||||
'http://blob/url/file.zip',
|
||||
authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る
|
||||
'file.zip',
|
||||
'11:22:33',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
'2023-05-26T11:22:33.444',
|
||||
256,
|
||||
'01',
|
||||
'DS2',
|
||||
'comment',
|
||||
'',
|
||||
optionItemList,
|
||||
false,
|
||||
);
|
||||
expect(result.jobNumber).toEqual('00000001');
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(notificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId', 'requestId'),
|
||||
`user_${typistUserId}`,
|
||||
{
|
||||
authorId: 'AUTHOR_ID',
|
||||
filename: 'file',
|
||||
id: '2',
|
||||
priority: 'High',
|
||||
uploadedAt: '2023-05-26T11:22:33.444',
|
||||
},
|
||||
);
|
||||
// タスクを取得
|
||||
const resultTask = await getTaskFromJobNumber(source, result.jobNumber);
|
||||
// タスクのチェックアウト権限を取得
|
||||
const resultCheckoutPermission = await getCheckoutPermissions(
|
||||
source,
|
||||
resultTask?.id ?? 0,
|
||||
);
|
||||
// タスクがあることを確認
|
||||
expect(resultTask).not.toBeNull();
|
||||
// 自動ルーティングが行われていることを確認
|
||||
expect(resultCheckoutPermission.length).toEqual(1);
|
||||
expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId);
|
||||
});
|
||||
it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
@ -1452,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',
|
||||
},
|
||||
@ -1570,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',
|
||||
},
|
||||
@ -1698,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',
|
||||
},
|
||||
@ -1826,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',
|
||||
},
|
||||
@ -1944,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',
|
||||
},
|
||||
@ -2094,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',
|
||||
},
|
||||
@ -2243,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',
|
||||
},
|
||||
@ -2393,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',
|
||||
},
|
||||
|
||||
@ -71,7 +71,8 @@ export class FilesService {
|
||||
/**
|
||||
* Uploads finished
|
||||
* @param url アップロード先Blob Storage(ファイル名含む)
|
||||
* @param authorId 自分自身(ログイン認証)したAuthorID
|
||||
* @param userId 自分自身(ログイン認証)したUserID
|
||||
* @param authorId 音声ファイルを管理するAuthorのAuthorID
|
||||
* @param fileName 音声ファイル名
|
||||
* @param duration 音声ファイルの録音時間(ミリ秒の整数値)
|
||||
* @param createdDate 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)'
|
||||
@ -248,7 +249,6 @@ export class FilesService {
|
||||
context,
|
||||
task.audio_file_id,
|
||||
user.account_id,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
const groupMembers =
|
||||
@ -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(
|
||||
@ -474,11 +485,7 @@ export class TasksService {
|
||||
if (!typist) {
|
||||
throw new Error(`typist not found. id=${externalId}`);
|
||||
}
|
||||
const { displayName: typistName, emailAddress: typistEmail } =
|
||||
getUserNameAndMailAddress(typist);
|
||||
if (!typistEmail) {
|
||||
throw new Error(`typist email not found. id=${externalId}`);
|
||||
}
|
||||
const { displayName: typistName } = getUserNameAndMailAddress(typist);
|
||||
|
||||
const primaryAdmin = usersInfo.find(
|
||||
(x) => x.id === primaryAdminExternalId,
|
||||
@ -495,7 +502,6 @@ export class TasksService {
|
||||
await this.sendgridService.sendMailWithU117(
|
||||
context,
|
||||
authorNotification ? authorEmail : null,
|
||||
typistEmail,
|
||||
authorName,
|
||||
task.file.file_name.replace('.zip', ''),
|
||||
typistName,
|
||||
@ -635,7 +641,6 @@ export class TasksService {
|
||||
context,
|
||||
audioFileId,
|
||||
user.account_id,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
// 通知を送信する
|
||||
@ -718,6 +723,80 @@ export class TasksService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した完了済みの音声ファイルに紐づくタスクを再開する(ステータスをPendingに変更する)
|
||||
* @param audioFileId
|
||||
* @param externalId
|
||||
* @param role
|
||||
* @returns reopen
|
||||
*/
|
||||
async reopen(
|
||||
context: Context,
|
||||
audioFileId: number,
|
||||
externalId: string,
|
||||
role: Roles[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.reopen.name
|
||||
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
|
||||
);
|
||||
let user: User;
|
||||
try {
|
||||
// ユーザー取得
|
||||
user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.reopen.name}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// roleにAdminが含まれていれば、文字起こし担当でなくても再開できるため、ユーザーIDは指定しない
|
||||
await this.taskRepository.reopen(
|
||||
context,
|
||||
audioFileId,
|
||||
[TASK_STATUS.FINISHED],
|
||||
user.account_id,
|
||||
role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case TasksNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010603'),
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
case StatusNotMatchError:
|
||||
case TypistUserNotMatchError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010601'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.reopen.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した音声ファイルに紐づくタスクをbackupする
|
||||
* @param context
|
||||
@ -1043,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: {
|
||||
|
||||
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