From ef86d6888568a1506b384312adbf6fe45cc64b11 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Sat, 25 Oct 2025 19:07:59 -0600 Subject: [PATCH 01/14] WIP: test git tag triggers --- .../cdk.context.beta-example.json | 4 +- .../cdk.context.prod-example.json | 4 +- .../cdk.context.test-example.json | 4 +- .../pipeline/__init__.py | 19 ++++-- .../pipeline/frontend_pipeline.py | 58 +++++++++++++++++-- .../cdk.context.beta-example.json | 4 +- .../cdk.context.prod-example.json | 4 +- .../cdk.context.test-example.json | 4 +- backend/compact-connect/pipeline/__init__.py | 26 +++++++-- .../pipeline/backend_pipeline.py | 52 +++++++++++++++-- .../tests/app/test_pipeline.py | 48 +++++++++++++++ 11 files changed, 202 insertions(+), 25 deletions(-) diff --git a/backend/compact-connect-ui-app/cdk.context.beta-example.json b/backend/compact-connect-ui-app/cdk.context.beta-example.json index fb0ab02dc..f02418585 100644 --- a/backend/compact-connect-ui-app/cdk.context.beta-example.json +++ b/backend/compact-connect-ui-app/cdk.context.beta-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "beta-*" }, "beta": { "account_id": "222233334444", diff --git a/backend/compact-connect-ui-app/cdk.context.prod-example.json b/backend/compact-connect-ui-app/cdk.context.prod-example.json index 53bd78d0f..5695810f3 100644 --- a/backend/compact-connect-ui-app/cdk.context.prod-example.json +++ b/backend/compact-connect-ui-app/cdk.context.prod-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "prod-*" }, "prod": { "account_id": "000011112222", diff --git a/backend/compact-connect-ui-app/cdk.context.test-example.json b/backend/compact-connect-ui-app/cdk.context.test-example.json index 2a1071aa1..7ae79c3d4 100644 --- a/backend/compact-connect-ui-app/cdk.context.test-example.json +++ b/backend/compact-connect-ui-app/cdk.context.test-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "test-*" }, "test": { "account_id": "111122223333", diff --git a/backend/compact-connect-ui-app/pipeline/__init__.py b/backend/compact-connect-ui-app/pipeline/__init__.py index fb1e20b9e..f7a1b6dc1 100644 --- a/backend/compact-connect-ui-app/pipeline/__init__.py +++ b/backend/compact-connect-ui-app/pipeline/__init__.py @@ -103,8 +103,8 @@ def __init__( **kwargs, ) - # Allows us to override the default branching scheme for the test environment, via context variable - pre_prod_trigger_branch = self.pipeline_environment_context.get('pre_prod_trigger_branch', 'development') + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] self.pre_prod_frontend_pipeline = FrontendPipeline( self, @@ -113,7 +113,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - source_branch=pre_prod_trigger_branch, + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, @@ -158,6 +159,9 @@ def __init__( **kwargs, ) + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] + self.beta_frontend_pipeline = FrontendPipeline( self, 'BetaFrontendPipeline', @@ -165,7 +169,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - source_branch='main', + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, @@ -210,6 +215,9 @@ def __init__( **kwargs, ) + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] + self.prod_frontend_pipeline = FrontendPipeline( self, 'ProdFrontendPipeline', @@ -217,7 +225,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - source_branch='main', + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, diff --git a/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py b/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py index 059c9d315..4ec7d94eb 100644 --- a/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py +++ b/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py @@ -29,8 +29,8 @@ class FrontendPipeline(CdkCodePipeline): Deployment Flow: 1. Backend Pipeline completes deployment of infrastructure resources - 2. Backend Pipeline triggers this Frontend Pipeline via AWS CLI command - 3. This pipeline pulls the same source code but synthesizes only frontend resources + 2. Backend Pipeline triggers this Frontend Pipeline via AWS CLI command with specific commit ID + 3. This pipeline pulls the EXACT SAME source code revision that triggered the backend 4. Frontend application deploys using configuration values created by the Backend Pipeline and stored in SSM Parameter Store """ @@ -44,7 +44,8 @@ def __init__( github_repo_string: str, cdk_path: str, connection_arn: str, - source_branch: str, + default_branch: str, + git_tag_trigger_pattern: str, access_logs_bucket: IBucket, encryption_key: IKey, alarm_topic: ITopic, @@ -54,6 +55,15 @@ def __init__( removal_policy: RemovalPolicy, **kwargs, ): + """ + Initialize the FrontendPipeline. + + :param default_branch: The git branch to use as the source for manual starts only. + When triggered by backend pipeline, the specific commit ID is used instead. + :param git_tag_trigger_pattern: The git tag pattern for trigger configuration. Note: This pipeline + does not automatically trigger on git events (trigger_on_push=False), + but the pattern is configured for consistency with the backend pipeline. + """ artifact_bucket = Bucket( scope, f'{construct_id}ArtifactsBucket', @@ -90,7 +100,7 @@ def __init__( 'Synth', input=CodePipelineSource.connection( repo_string=github_repo_string, - branch=source_branch, + branch=default_branch, # This pipeline is triggered by the backend pipeline, so we don't # want push events to trigger it. This prevents duplicate deployments # since both pipelines use the same source code. @@ -131,6 +141,8 @@ def __init__( **kwargs, ) self._ssm_parameter = ssm_parameter + self._git_tag_trigger_pattern = git_tag_trigger_pattern + self._github_repo_string = github_repo_string self._encryption_key = encryption_key self._alarm_topic = alarm_topic @@ -176,6 +188,7 @@ def build_pipeline(self) -> None: self._add_alarms() self._add_codebuild_pipeline_role_override() + self._configure_git_tag_trigger() def _add_alarms(self): NotificationRule( @@ -290,3 +303,40 @@ def _add_codebuild_pipeline_role_override(self): # Now, remove the unused role and default policy assets_node.node.try_remove_child('FileRole') + + def _configure_git_tag_trigger(self): + """ + Configure git tag-based trigger using CDK escape hatch. + + When triggers with filters are configured, AWS requires DetectChanges to be false + in the source action configuration. The trigger configuration replaces the default + change detection mechanism. + + The source action's branch (default_branch) is still used when the pipeline is + started manually, but automatic triggers are controlled by the git tag pattern. + """ + cfn_pipeline = self.pipeline.node.default_child + + # Add the Triggers property + cfn_pipeline.add_property_override('Triggers', [ + { + 'ProviderType': 'CodeStarSourceConnection', + 'GitConfiguration': { + 'SourceActionName': self._github_repo_string.replace('/', '_'), + 'Push': [ + { + 'Tags': { + 'Includes': [self._git_tag_trigger_pattern] + } + } + ] + } + } + ]) + + # Set DetectChanges to false in the source action + # The source action is in Stages[0].Actions[0] (first action of Source stage) + cfn_pipeline.add_property_override( + 'Stages.0.Actions.0.Configuration.DetectChanges', + False + ) diff --git a/backend/compact-connect/cdk.context.beta-example.json b/backend/compact-connect/cdk.context.beta-example.json index 71eb07d2b..a0bd627b5 100644 --- a/backend/compact-connect/cdk.context.beta-example.json +++ b/backend/compact-connect/cdk.context.beta-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "beta-*" }, "beta": { "account_id": "222233334444", diff --git a/backend/compact-connect/cdk.context.prod-example.json b/backend/compact-connect/cdk.context.prod-example.json index 5373d6fdb..b2f2fa461 100644 --- a/backend/compact-connect/cdk.context.prod-example.json +++ b/backend/compact-connect/cdk.context.prod-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "prod-*" }, "prod": { "account_id": "000011112222", diff --git a/backend/compact-connect/cdk.context.test-example.json b/backend/compact-connect/cdk.context.test-example.json index 14cd25a8f..9046713c8 100644 --- a/backend/compact-connect/cdk.context.test-example.json +++ b/backend/compact-connect/cdk.context.test-example.json @@ -6,7 +6,9 @@ "pipeline": { "account_id": "000000000000", "region": "us-east-1", - "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111" + "connection_arn": "arn:aws:codestar-connections:us-east-1:000000000000:connection/11111111-1111-1111-111111111111", + "default_branch": "main", + "git_tag_trigger_pattern": "test-*" }, "test": { "account_id": "111122223333", diff --git a/backend/compact-connect/pipeline/__init__.py b/backend/compact-connect/pipeline/__init__.py index 87886781c..c31635c6a 100644 --- a/backend/compact-connect/pipeline/__init__.py +++ b/backend/compact-connect/pipeline/__init__.py @@ -126,7 +126,12 @@ def _generate_frontend_pipeline_trigger_step(self): ) return CodeBuildStep( 'TriggerFrontendPipeline', - commands=[f'aws codepipeline start-pipeline-execution --name {self._get_frontend_pipeline_name()}'], + commands=[ + # Trigger frontend pipeline with the same commit ID + f'aws codepipeline start-pipeline-execution --name {self._get_frontend_pipeline_name()} ' + f'--source-revisions actionName={self.github_repo_string.replace("/", "_")}' + ',revisionType=COMMIT_ID,revisionValue=$CODEBUILD_RESOLVED_SOURCE_VERSION' + ], role=trigger_frontend_pipeline_role, ) @@ -173,8 +178,8 @@ def __init__( **kwargs, ) - # Allows us to override the default branching scheme for the test environment, via context variable - pre_prod_trigger_branch = self.pipeline_environment_context.get('pre_prod_trigger_branch', 'development') + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] self.pre_prod_pipeline = BackendPipeline( self, @@ -183,7 +188,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - trigger_branch=pre_prod_trigger_branch, + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, @@ -235,6 +241,9 @@ def __init__( **kwargs, ) + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] + self.beta_backend_pipeline = BackendPipeline( self, 'BetaBackendPipeline', @@ -242,7 +251,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - trigger_branch='main', + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, @@ -297,6 +307,9 @@ def __init__( if not self.backup_config or not self.ssm_context['environments'][PROD_ENVIRONMENT_NAME].get('backup_enabled'): raise ValueError('Backups must be enabled for production environment.') + default_branch = self.pipeline_environment_context['default_branch'] + git_tag_trigger_pattern = self.pipeline_environment_context['git_tag_trigger_pattern'] + self.prod_pipeline = BackendPipeline( self, 'ProdBackendPipeline', @@ -304,7 +317,8 @@ def __init__( github_repo_string=self.github_repo_string, cdk_path=cdk_path, connection_arn=self.connection_arn, - trigger_branch='main', + default_branch=default_branch, + git_tag_trigger_pattern=git_tag_trigger_pattern, encryption_key=pipeline_shared_encryption_key, alarm_topic=pipeline_alarm_topic, access_logs_bucket=self.access_logs_bucket, diff --git a/backend/compact-connect/pipeline/backend_pipeline.py b/backend/compact-connect/pipeline/backend_pipeline.py index e5607c294..c930c368e 100644 --- a/backend/compact-connect/pipeline/backend_pipeline.py +++ b/backend/compact-connect/pipeline/backend_pipeline.py @@ -28,8 +28,9 @@ class BackendPipeline(CdkCodePipeline): 2. The Frontend Pipeline then deploys the frontend application using those resources Deployment Flow: - - IS triggered by GitHub pushes (trigger_on_push=True) - - Triggers the Frontend Pipeline after successful deployment + - Automatically triggered by git tags matching the specified pattern (e.g., 'prod-*') + - Can be manually started, which will use the default_branch for source code + - Triggers the Frontend Pipeline after successful deployment with the EXACT SAME commit ID """ def __init__( @@ -41,7 +42,8 @@ def __init__( github_repo_string: str, cdk_path: str, connection_arn: str, - trigger_branch: str, + default_branch: str, + git_tag_trigger_pattern: str, access_logs_bucket: IBucket, encryption_key: IKey, alarm_topic: ITopic, @@ -51,6 +53,14 @@ def __init__( removal_policy: RemovalPolicy, **kwargs, ): + """ + Initialize the BackendPipeline. + + :param default_branch: The git branch to use when the pipeline is started manually. + This branch is NOT used for automatic triggers. + :param git_tag_trigger_pattern: The git tag pattern (glob format) that will automatically + trigger the pipeline (e.g., 'prod-*', 'beta-*', 'test-*'). + """ artifact_bucket = Bucket( scope, f'{construct_id}ArtifactsBucket', @@ -88,7 +98,7 @@ def __init__( 'Synth', input=CodePipelineSource.connection( repo_string=github_repo_string, - branch=trigger_branch, + branch=default_branch, trigger_on_push=True, # Arn format: # arn:aws:codeconnections:us-east-1:111122223333:connection/ @@ -126,6 +136,8 @@ def __init__( **kwargs, ) self._ssm_parameter = ssm_parameter + self._git_tag_trigger_pattern = git_tag_trigger_pattern + self._github_repo_string = github_repo_string self._encryption_key = encryption_key self._alarm_topic = alarm_topic @@ -171,6 +183,7 @@ def build_pipeline(self) -> None: self._add_alarms() self._add_codebuild_pipeline_role_override() + self._configure_git_tag_trigger() def _add_alarms(self): NotificationRule( @@ -285,3 +298,34 @@ def _add_codebuild_pipeline_role_override(self): # Now, remove the unused role and default policy assets_node.node.try_remove_child('FileRole') + + def _configure_git_tag_trigger(self): + """ + Configure git tag-based trigger using CDK escape hatch. + + When triggers with filters are configured, AWS requires DetectChanges to be false + in the source action configuration. The trigger configuration replaces the default + change detection mechanism. + + The source action's branch (default_branch) is still used when the pipeline is + started manually, but automatic triggers are controlled by the git tag pattern. + """ + cfn_pipeline = self.pipeline.node.default_child + + # Add the Triggers property + cfn_pipeline.add_property_override( + 'Triggers', + [ + { + 'ProviderType': 'CodeStarSourceConnection', + 'GitConfiguration': { + 'SourceActionName': self._github_repo_string.replace('/', '_'), + 'Push': [{'Tags': {'Includes': [self._git_tag_trigger_pattern]}}], + }, + } + ], + ) + + # Set DetectChanges to false in the source action + # The source action is in Stages[0].Actions[0] (first action of Source stage) + cfn_pipeline.add_property_override('Stages.0.Actions.0.Configuration.DetectChanges', False) diff --git a/backend/compact-connect/tests/app/test_pipeline.py b/backend/compact-connect/tests/app/test_pipeline.py index 4dde7e14d..8f6c79e98 100644 --- a/backend/compact-connect/tests/app/test_pipeline.py +++ b/backend/compact-connect/tests/app/test_pipeline.py @@ -415,6 +415,54 @@ def test_pipeline_uses_predictable_roles_for_actions(self): {'RoleArn': {'Fn::GetAtt': [Match.string_like_regexp('.*BackendCrossAccountRole.*'), 'Arn']}}, ) + def test_pipeline_git_tag_triggers(self): + """Test that pipelines are configured with git tag triggers.""" + test_cases = [ + (self.app.test_backend_pipeline_stack, 'test-*'), + (self.app.beta_backend_pipeline_stack, 'beta-*'), + (self.app.prod_backend_pipeline_stack, 'prod-*'), + ] + + for stack, expected_pattern in test_cases: + with self.subTest(stack=stack.stack_name): + template = Template.from_stack(stack) + + # Verify all git tag trigger properties in a single call + template.has_resource_properties( + 'AWS::CodePipeline::Pipeline', + { + 'Triggers': [ + { + 'ProviderType': 'CodeStarSourceConnection', + 'GitConfiguration': { + 'SourceActionName': 'csg-org_CompactConnect', + 'Push': [{'Tags': {'Includes': [expected_pattern]}}], + }, + } + ], + 'Stages': Match.array_with( + [ + Match.object_like( + { + 'Name': 'Source', + 'Actions': Match.array_with( + [ + Match.object_like( + { + 'Configuration': Match.object_like( + {'DetectChanges': False, 'BranchName': Match.any_value()} + ) + } + ) + ] + ), + } + ) + ] + ), + }, + ) + class TestBackendPipelineVulnerable(TestCase): @patch.dict(os.environ, {'CDK_DEFAULT_ACCOUNT': '000000000000', 'CDK_DEFAULT_REGION': 'us-east-1'}) From 5cfc0688d8b4d4d7d6564bdd78278026dd68d2c0 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Tue, 28 Oct 2025 12:51:07 -0600 Subject: [PATCH 02/14] WIP: Investigative info - Initial data layer updates - @todo: UI updates - @todo: Work with design on minor figma updates - @todo: Test once backend is ready --- .../src/components/LicenseCard/LicenseCard.ts | 3 + .../Investigation/Investigation.model.spec.ts | 157 ++++++++++ .../Investigation/Investigation.model.ts | 106 +++++++ .../src/models/License/License.model.spec.ts | 34 +++ webroot/src/models/License/License.model.ts | 14 + .../models/Licensee/Licensee.model.spec.ts | 55 ++++ webroot/src/models/Licensee/Licensee.model.ts | 55 +++- webroot/src/network/data.api.ts | 86 ++++++ webroot/src/network/licenseApi/data.api.ts | 135 ++++++++ webroot/src/network/mocks/mock.data.api.ts | 74 +++++ webroot/src/store/users/users.actions.ts | 120 ++++++++ webroot/src/store/users/users.mutations.ts | 60 ++++ webroot/src/store/users/users.spec.ts | 288 ++++++++++++++++++ 13 files changed, 1186 insertions(+), 1 deletion(-) create mode 100644 webroot/src/models/Investigation/Investigation.model.spec.ts create mode 100644 webroot/src/models/Investigation/Investigation.model.ts diff --git a/webroot/src/components/LicenseCard/LicenseCard.ts b/webroot/src/components/LicenseCard/LicenseCard.ts index e1355de10..c2e62a72e 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -73,8 +73,11 @@ class LicenseCard extends mixins(MixinForm) { isEncumberLicenseModalSuccess = false; isUnencumberLicenseModalDisplayed = false; isUnencumberLicenseModalSuccess = false; + isInvestigationLicenseModalDisplayed = false; + isInvestigationLicenseModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + selectedInvestigationId: string | null = null; modalErrorMessage = ''; // diff --git a/webroot/src/models/Investigation/Investigation.model.spec.ts b/webroot/src/models/Investigation/Investigation.model.spec.ts new file mode 100644 index 000000000..37cffa572 --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.spec.ts @@ -0,0 +1,157 @@ +// +// Investigation.model.spec.ts +// CompactConnect +// +// Created by InspiringApps on 10/28/2025. +// + +import chaiMatchPattern from 'chai-match-pattern'; +import chai from 'chai'; +import { serverDateFormat, displayDateFormat } from '@/app.config'; +import { Investigation, InvestigationSerializer } from '@models/Investigation/Investigation.model'; +import { State } from '@models/State/State.model'; +import i18n from '@/i18n'; +import moment from 'moment'; + +chai.use(chaiMatchPattern); + +const { expect } = chai; + +describe('Investigation model', () => { + before(() => { + const { tm: $tm, t: $t } = i18n.global; + + (window as any).Vue = { + config: { + globalProperties: { + $tm, + $t, + } + } + }; + i18n.global.locale = 'en'; + }); + it('should create an Investigation model with expected defaults', () => { + const investigation = new Investigation(); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(null); + expect(investigation.compactType).to.equal(null); + expect(investigation.providerId).to.equal(null); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.type).to.equal(null); + expect(investigation.startDate).to.equal(null); + expect(investigation.updateDate).to.equal(null); + + // Test methods + expect(investigation.startDateDisplay()).to.equal(''); + expect(investigation.updateDateDisplay()).to.equal(''); + expect(investigation.hasEndDate()).to.equal(false); + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values', () => { + const data = { + id: 'test-id', + compactType: 'test-compactType', + providerId: 'test-providerId', + state: new State(), + type: 'test-type', + startDate: 'test-startDate', + updateDate: 'test-endDate', + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(data.id); + expect(investigation.compactType).to.equal(data.compactType); + expect(investigation.providerId).to.equal(data.providerId); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.type).to.equal(data.type); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.startDateDisplay()).to.equal('Invalid date'); + expect(investigation.updateDateDisplay()).to.equal('Invalid date'); + expect(investigation.hasEndDate()).to.equal(true); + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values (startDate but no endDate)', () => { + const data = { + startDate: moment().format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.updateDate).to.equal(null); + + // Test methods + expect(investigation.isActive()).to.equal(true); + }); + it('should create an Investigation model with specific values (endDate but no startDate)', () => { + const data = { + updateDate: moment().add(1, 'day').format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(null); + expect(investigation.updateDate).to.equal(data.updateDate); + + // Test methods + expect(investigation.isActive()).to.equal(true); + }); + it('should create an Investigation model with specific values (updateDate of today should count as lifted)', () => { + const data = { + startDate: moment().format(serverDateFormat), + updateDate: moment().format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.updateDate).to.equal(data.updateDate); + + // Test methods + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values through serializer', () => { + const data = { + investigationId: 'test-id', + compact: 'aslp', + providerId: 'test-providerId', + jurisdiction: 'al', + type: 'test-type', + creationDate: moment.utc().format(serverDateFormat), + dateOfUpdate: moment.utc().add(1, 'day').format(serverDateFormat), + }; + const investigation = InvestigationSerializer.fromServer(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(data.investigationId); + expect(investigation.compactType).to.equal(data.compact); + expect(investigation.providerId).to.equal(data.providerId); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.state.name()).to.equal('Alabama'); + expect(investigation.type).to.equal(data.type); + expect(investigation.startDate).to.equal(data.creationDate); + expect(investigation.updateDate).to.equal(data.dateOfUpdate); + + // Test methods + expect(investigation.startDateDisplay()).to.equal( + moment(data.creationDate, serverDateFormat).format(displayDateFormat) + ); + expect(investigation.updateDateDisplay()).to.equal( + moment(data.dateOfUpdate, serverDateFormat).format(displayDateFormat) + ); + expect(investigation.hasEndDate()).to.equal(true); + expect(investigation.isActive()).to.equal(true); + }); +}); diff --git a/webroot/src/models/Investigation/Investigation.model.ts b/webroot/src/models/Investigation/Investigation.model.ts new file mode 100644 index 000000000..8a1a18b2e --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.ts @@ -0,0 +1,106 @@ +// +// Investigation.ts +// CompactConnect +// +// Created by InspiringApps on 10/28/2025. +// + +import { deleteUndefinedProperties } from '@models/_helpers'; +import { serverDateFormat } from '@/app.config'; +import { dateDisplay } from '@models/_formatters/date'; +import { CompactType } from '@models/Compact/Compact.model'; +import { State } from '@models/State/State.model'; +import moment from 'moment'; +import { StatsigClient } from '@statsig/js-client'; + +// ======================================================== +// = Interface = +// ======================================================== +export interface InterfaceInvestigationCreate { + id?: string | null; + compactType?: CompactType | null; + providerId?: string | null; + state?: State; + type?: string | null; + startDate?: string | null; + updateDate?: string | null; +} + +// ======================================================== +// = Model = +// ======================================================== +export class Investigation implements InterfaceInvestigationCreate { + public $tm?: any = () => []; + public $features?: StatsigClient | null = null; + public id? = null; + public compactType? = null; + public providerId? = null; + public state? = new State(); + public type? = null; + public startDate? = null; + public updateDate? = null; + + constructor(data?: InterfaceInvestigationCreate) { + const cleanDataObject = deleteUndefinedProperties(data); + const global = window as any; + const { $tm, $features } = global.Vue?.config?.globalProperties || {}; + + this.$tm = $tm; + this.$features = $features; + + Object.assign(this, cleanDataObject); + } + + // Helper methods + public startDateDisplay(): string { + return dateDisplay(this.startDate); + } + + public updateDateDisplay(): string { + return dateDisplay(this.updateDate); + } + + public hasEndDate(): boolean { + return Boolean(this.updateDate); + } + + public isActive(): boolean { + // Determine whether the investigation is currently in effect + const { startDate, updateDate: endDate } = this; + const startDateMoment = (startDate) ? moment(startDate, serverDateFormat) : null; + const endDateMoment = (endDate) ? moment(endDate, serverDateFormat) : null; + const now = moment(); + const isAfterStartDate = (startDateMoment?.isValid()) ? now.isSameOrAfter(startDateMoment, 'day') : false; + const isBeforeEndDate = (endDateMoment?.isValid()) ? now.isBefore(endDateMoment, 'day') : false; + let isInvestigationActive = false; + + if (isAfterStartDate && isBeforeEndDate) { + isInvestigationActive = true; + } else if (startDate && !endDate && isAfterStartDate) { + isInvestigationActive = true; + } else if (endDate && !startDate && isBeforeEndDate) { + isInvestigationActive = true; + } + + return isInvestigationActive; + } +} + +// ======================================================== +// = Serializer = +// ======================================================== +export class InvestigationSerializer { + static fromServer(json: any): Investigation { + const investigationData = { + id: json.investigationId, + compactType: json.compact, + providerId: json.providerId, + state: new State({ abbrev: json.jurisdiction }), + type: json.type, + startDate: json.creationDate, + updateDate: json.dateOfUpdate, + }; + + return new Investigation(investigationData); + } +} diff --git a/webroot/src/models/License/License.model.spec.ts b/webroot/src/models/License/License.model.spec.ts index ccc47cba6..611090012 100644 --- a/webroot/src/models/License/License.model.spec.ts +++ b/webroot/src/models/License/License.model.spec.ts @@ -19,6 +19,7 @@ import { State } from '@models/State/State.model'; import { Address } from '@models/Address/Address.model'; import { LicenseHistoryItem } from '@models/LicenseHistoryItem/LicenseHistoryItem.model'; import { AdverseAction } from '@models/AdverseAction/AdverseAction.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import i18n from '@/i18n'; import moment from 'moment'; @@ -64,6 +65,7 @@ describe('License model', () => { expect(license.statusDescription).to.equal(null); expect(license.eligibility).to.equal(EligibilityStatus.INELIGIBLE); expect(license.adverseActions).to.matchPattern([]); + expect(license.investigations).to.matchPattern([]); // Test methods expect(license.issueDateDisplay()).to.equal(''); @@ -77,6 +79,7 @@ describe('License model', () => { expect(license.displayName()).to.equal('Unknown'); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(false); }); it('should create a License with specific values', () => { const data = { @@ -99,6 +102,7 @@ describe('License model', () => { statusDescription: 'test-status-desc', eligibility: EligibilityStatus.ELIGIBLE, adverseActions: [new AdverseAction()], + investigations: [new Investigation()], }; const license = new License(data); @@ -123,6 +127,7 @@ describe('License model', () => { expect(license.statusDescription).to.equal(data.statusDescription); expect(license.eligibility).to.equal(data.eligibility); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal('Invalid date'); @@ -137,6 +142,7 @@ describe('License model', () => { expect(license.displayName(', ', true)).to.equal('Unknown, AUD'); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(false); }); it('should create a License with specific values (custom displayName delimiter)', () => { const data = { @@ -181,6 +187,17 @@ describe('License model', () => { effectiveLiftDate: moment().add(1, 'day').format(serverDateFormat), }, ], + investigations: [ + { + investigationId: 'test-id', + compact: CompactType.ASLP, + providerId: 'test-provider-id', + jurisdiction: 'al', + type: 'investigation', + creationDate: moment().subtract(1, 'day').format(serverDateFormat), + dateOfUpdate: moment().add(1, 'day').format(serverDateFormat), + }, + ], }; const license = LicenseSerializer.fromServer(data); @@ -203,6 +220,8 @@ describe('License model', () => { expect(license.eligibility).to.equal(data.compactEligibility); expect(license.adverseActions).to.be.an('array').with.length(1); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); + expect(license.investigations).to.be.an('array').with.length(1); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal( @@ -222,6 +241,7 @@ describe('License model', () => { expect(license.licenseTypeAbbreviation()).to.equal('AUD'); expect(license.isEncumbered()).to.equal(true); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(true); }); it('should create a privilege with specific values through serializer', () => { const data = { @@ -244,6 +264,17 @@ describe('License model', () => { effectiveLiftDate: moment().subtract(3, 'months').format(serverDateFormat), }, ], + investigations: [ + { + investigationId: 'test-id', + compact: CompactType.ASLP, + providerId: 'test-provider-id', + jurisdiction: 'al', + type: 'investigation', + creationDate: moment().subtract(1, 'day').format(serverDateFormat), + dateOfUpdate: moment().add(1, 'day').format(serverDateFormat), + }, + ], attestations: [ { attestationId: 'personal-information-address-attestation', @@ -550,6 +581,8 @@ describe('License model', () => { expect(license.adverseActions).to.be.an('array').with.length(1); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); expect(license.adverseActions[0].endDate).to.equal(data.adverseActions[0].effectiveLiftDate); + expect(license.investigations).to.be.an('array').with.length(1); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal( @@ -573,6 +606,7 @@ describe('License model', () => { expect(license.history.length).to.equal(0); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(true); + expect(license.isUnderInvestigation()).to.equal(true); }); it('should populate isDeactivated correctly given license history (deactivation)', () => { const data = { diff --git a/webroot/src/models/License/License.model.ts b/webroot/src/models/License/License.model.ts index 9093e8ffb..318611453 100644 --- a/webroot/src/models/License/License.model.ts +++ b/webroot/src/models/License/License.model.ts @@ -13,6 +13,7 @@ import { State } from '@models/State/State.model'; import { LicenseHistoryItem } from '@models/LicenseHistoryItem/LicenseHistoryItem.model'; import { Address, AddressSerializer } from '@models/Address/Address.model'; import { AdverseAction, AdverseActionSerializer } from '@models/AdverseAction/AdverseAction.model'; +import { Investigation, InvestigationSerializer } from '@models/Investigation/Investigation.model'; import moment from 'moment'; import { StatsigClient } from '@statsig/js-client'; @@ -63,6 +64,7 @@ export interface InterfaceLicense { statusDescription?: string | null, eligibility?: EligibilityStatus, adverseActions?: Array, + investigations?: Array, } // ======================================================== @@ -91,6 +93,7 @@ export class License implements InterfaceLicense { public statusDescription? = null; public eligibility? = EligibilityStatus.INELIGIBLE; public adverseActions? = []; + public investigations? = []; constructor(data?: InterfaceLicense) { const cleanDataObject = deleteUndefinedProperties(data); @@ -183,6 +186,10 @@ export class License implements InterfaceLicense { return isWithinWaitPeriod; } + + public isUnderInvestigation(): boolean { + return this.investigations?.some((investigation: Investigation) => investigation.isActive()) || false; + } } // ======================================================== @@ -217,6 +224,7 @@ export class LicenseSerializer { ? json.compactEligibility : EligibilityStatus.NA, adverseActions: [] as Array, + investigations: [] as Array, }; if (Array.isArray(json.adverseActions)) { @@ -225,6 +233,12 @@ export class LicenseSerializer { }); } + if (Array.isArray(json.investigations)) { + json.investigations.forEach((serverInvestigation) => { + licenseData.investigations.push(InvestigationSerializer.fromServer(serverInvestigation)); + }); + } + return new License(licenseData); } } diff --git a/webroot/src/models/Licensee/Licensee.model.spec.ts b/webroot/src/models/Licensee/Licensee.model.spec.ts index 146c374cc..7bfd8ffde 100644 --- a/webroot/src/models/Licensee/Licensee.model.spec.ts +++ b/webroot/src/models/Licensee/Licensee.model.spec.ts @@ -18,6 +18,7 @@ import { EligibilityStatus } from '@models/License/License.model'; import { MilitaryAffiliation } from '@models/MilitaryAffiliation/MilitaryAffiliation.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import { State } from '@models/State/State.model'; import i18n from '@/i18n'; import moment from 'moment'; @@ -90,6 +91,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values', () => { const data = { @@ -198,6 +203,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values (empty state name fallbacks)', () => { const licensee = new Licensee(); @@ -497,6 +506,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values through serializer (with inactive best license)', () => { const data = { @@ -631,6 +644,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values through serializer (with initiliazing military status)', () => { const data = { @@ -1111,6 +1128,44 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(true); expect(licensee.isEncumbered()).to.equal(true); }); + it('should create a Licensee with under-investigation licenses and privileges', () => { + const underInvestigationLicense = new License({ + licenseNumber: 'encumbered-license', + investigations: [new Investigation({ + state: new State({ abbrev: 'al' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + })], + }); + const underInvestigationPrivilege = new License({ + licenseNumber: 'encumbered-privilege', + investigations: [ + new Investigation({ + state: new State({ abbrev: 'al' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + }), + new Investigation({ + state: new State({ abbrev: 'co' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + }), + ], + }); + const licensee = new Licensee({ + licenses: [underInvestigationLicense], + privileges: [underInvestigationPrivilege], + }); + + // Test encumbered methods + expect(licensee.hasUnderInvestigationLicenses()).to.equal(true); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(true); + expect(licensee.isUnderInvestigation()).to.equal(true); + expect(licensee.underInvestigationStates()).to.matchPattern([ + new State({ abbrev: 'al' }), + new State({ abbrev: 'co' }), + ]); + }); it(`should handle 'unknown' currentHomeJurisdiction by falling back to licenseJurisdiction`, () => { const data = { providerId: 'test-id', diff --git a/webroot/src/models/Licensee/Licensee.model.ts b/webroot/src/models/Licensee/Licensee.model.ts index 5eb8c8f2a..3c2a49b77 100644 --- a/webroot/src/models/Licensee/Licensee.model.ts +++ b/webroot/src/models/Licensee/Licensee.model.ts @@ -17,6 +17,7 @@ import { EligibilityStatus } from '@models/License/License.model'; import { MilitaryAffiliation, MilitaryAffiliationSerializer } from '@models/MilitaryAffiliation/MilitaryAffiliation.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import { State } from '@models/State/State.model'; import moment from 'moment'; import { StatsigClient } from '@statsig/js-client'; @@ -329,6 +330,57 @@ export class Licensee implements InterfaceLicensee { privilege.isLatestLiftedEncumbranceWithinWaitPeriod()) || false; } + public hasUnderInvestigationLicenses(): boolean { + return this.licenses?.some((license: License) => license.isUnderInvestigation()) || false; + } + + public hasUnderInvestigationPrivileges(): boolean { + return this.privileges?.some((privilege: License) => privilege.isUnderInvestigation()) || false; + } + + public isUnderInvestigation(): boolean { + return this.hasUnderInvestigationLicenses() || this.hasUnderInvestigationPrivileges(); + } + + public underInvestigationStates(): Array { + const investigationStates: Array = []; + const investigationStatesAbbrev: Array = []; + + this.licenses?.forEach((license: License) => { + if (license.isUnderInvestigation()) { + license.investigations?.forEach((investigation: Investigation) => { + const investigationState = investigation.state; + const investigationStateAbbrev = investigation.state?.abbrev; + + if (investigationState + && investigationStateAbbrev + && !investigationStatesAbbrev.includes(investigationStateAbbrev)) { + investigationStates.push(investigationState); + investigationStatesAbbrev.push(investigationStateAbbrev); + } + }); + } + }); + + this.privileges?.forEach((privilege: License) => { + if (privilege.isUnderInvestigation()) { + privilege.investigations?.forEach((investigation: Investigation) => { + const investigationState = investigation.state; + const investigationStateAbbrev = investigation.state?.abbrev; + + if (investigationState + && investigationStateAbbrev + && !investigationStatesAbbrev.includes(investigationStateAbbrev)) { + investigationStates.push(investigationState); + investigationStatesAbbrev.push(investigationStateAbbrev); + } + }); + } + }); + + return investigationStates; + } + public purchaseEligibleLicenses(): Array { return this.activeHomeJurisdictionLicenses() .filter((license: License) => (license.eligibility === EligibilityStatus.ELIGIBLE)); @@ -338,7 +390,8 @@ export class Licensee implements InterfaceLicensee { return !!this.purchaseEligibleLicenses().length && !this.isMilitaryStatusInitializing() && !this.isEncumbered() - && !this.hasEncumbranceLiftedWithinWaitPeriod(); + && !this.hasEncumbranceLiftedWithinWaitPeriod() + && !this.isUnderInvestigation(); } } diff --git a/webroot/src/network/data.api.ts b/webroot/src/network/data.api.ts index cdde3cb5a..2f16e0176 100644 --- a/webroot/src/network/data.api.ts +++ b/webroot/src/network/data.api.ts @@ -212,6 +212,49 @@ export class DataApi { ); } + /** + * POST Create License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + return licenseDataApi.createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ); + } + + /** + * Update License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @return {Promise} The server response. + */ + public updateLicenseInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + return licenseDataApi.updateLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + ); + } + /** * DELETE Privilege for a licensee. * @param {string} compact The compact string ID (aslp, octp, coun). @@ -280,6 +323,49 @@ export class DataApi { ); } + /** + * POST Create Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public createPrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + return licenseDataApi.createPrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ); + } + + /** + * Update Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @return {Promise} The server response. + */ + public updatePrivilegeInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + return licenseDataApi.updatePrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + ); + } + /** * GET Licensee SSN by ID. * @param {string} compact A compact type. diff --git a/webroot/src/network/licenseApi/data.api.ts b/webroot/src/network/licenseApi/data.api.ts index 6dfc3b2bd..5ea8700d3 100644 --- a/webroot/src/network/licenseApi/data.api.ts +++ b/webroot/src/network/licenseApi/data.api.ts @@ -316,6 +316,73 @@ export class LicenseDataApi implements DataApiInterface { return serverResponse; } + /** + * POST Create License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public async createLicenseInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string + ) { + const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/licenses/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation`, {}); + + return serverResponse; + } + + /** + * PATCH Update License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {object} [encumbrance] Optional encumbrance config to add to the license. + * @param {string} encumbranceType The discipline action type. + * @param {string} npdbCategory The NPDB category name. + * @param {Array} npdbCategories The NPDB category list. + * @param {string} startDate The encumber start date. + * @return {Promise} The server response. + */ + public async updateLicenseInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string, + investigationId: string, + encumbrance?: { + encumbranceType: string, + npdbCategory: string, + npdbCategories: Array, + startDate: string + } + ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + const serverResponse: any = await this.api.patch(`/v1/compacts/${compact}/providers/${licenseeId}/licenses/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation/${investigationId}`, { + ...(encumbrance + ? { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + } + : undefined + ), + }); + + return serverResponse; + } + /** * DELETE Privilege for a licensee. * @param {string} compact The compact string ID (aslp, octp, coun). @@ -403,6 +470,74 @@ export class LicenseDataApi implements DataApiInterface { return serverResponse; } + /** + * POST Create Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public async createPrivilegeInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string + ) { + const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation`, {}); + + return serverResponse; + } + + /** + * PATCH Update Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @param {string} encumbranceType The discipline action type. + * @param {string} npdbCategory The NPDB category name. + * @param {Array} npdbCategories The NPDB category list. + * @param {string} startDate The encumber start date. + * @return {Promise} The server response. + */ + public async updatePrivilegeInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string, + investigationId: string, + encumbrance?: { + encumbranceType: string, + npdbCategory: string, + npdbCategories: Array, + startDate: string + } + ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + const serverResponse: any = await this.api.patch(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation/${investigationId}`, { + ...(encumbrance + ? { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + } + : undefined + ), + }); + + return serverResponse; + } + /** * GET SSN for licensee by ID. * @param {string} licenseeId A licensee ID. diff --git a/webroot/src/network/mocks/mock.data.api.ts b/webroot/src/network/mocks/mock.data.api.ts index d4e0c3fb2..984884d50 100644 --- a/webroot/src/network/mocks/mock.data.api.ts +++ b/webroot/src/network/mocks/mock.data.api.ts @@ -278,6 +278,43 @@ export class DataApi { })); } + // Create License Investigation for a licensee. + public createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + if (!compact) { + return Promise.reject(new Error('failed license investigation create')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + })); + } + + // Update License Investigation for a licensee. + public updateLicenseInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + if (!compact) { + return Promise.reject(new Error('failed license investigation update')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance, + })); + } + // Delete Privilege for a licensee. public deletePrivilege(compact, licenseeId, privilegeState, licenseType) { if (!compact) { @@ -346,6 +383,43 @@ export class DataApi { })); } + // Create Privilege Investigation for a licensee. + public createPrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation create')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + })); + } + + // Update Privilege Investigation for a licensee. + public updatePrivilegeInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation update')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance, + })); + } + // Get full SSN for licensee public getLicenseeSsn(compact, licenseeId) { return wait(500).then(() => ({ diff --git a/webroot/src/store/users/users.actions.ts b/webroot/src/store/users/users.actions.ts index 3c3940db3..5e5145589 100644 --- a/webroot/src/store/users/users.actions.ts +++ b/webroot/src/store/users/users.actions.ts @@ -192,6 +192,66 @@ export default { unencumberLicenseFailure: ({ commit }, error: Error) => { commit(MutationTypes.UNENCUMBER_LICENSE_FAILURE, error); }, + // CREATE INVESTIGATION FOR USER LICENSE + createInvestigationLicenseRequest: async ({ commit, dispatch }, { + compact, + licenseeId, + licenseState, + licenseType, + }: any) => { + commit(MutationTypes.CREATE_INVESTIGATION_LICENSE_REQUEST); + return dataApi.createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ).then(async (response) => { + dispatch('createInvestigationLicenseSuccess'); + + return response; + }).catch((error) => { + dispatch('createInvestigationLicenseFailure', error); + throw error; + }); + }, + createInvestigationLicenseSuccess: ({ commit }) => { + commit(MutationTypes.CREATE_INVESTIGATION_LICENSE_SUCCESS); + }, + createInvestigationLicenseFailure: ({ commit }, error: Error) => { + commit(MutationTypes.CREATE_INVESTIGATION_LICENSE_FAILURE, error); + }, + // UPDATE INVESTIGATION FOR USER LICENSE + updateInvestigationLicenseRequest: async ({ commit, dispatch }, { + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + }: any) => { + commit(MutationTypes.UPDATE_INVESTIGATION_LICENSE_REQUEST); + return dataApi.updateLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + ).then(async (response) => { + dispatch('updateInvestigationLicenseSuccess'); + + return response; + }).catch((error) => { + dispatch('updateInvestigationLicenseFailure', error); + throw error; + }); + }, + updateInvestigationLicenseSuccess: ({ commit }) => { + commit(MutationTypes.UPDATE_INVESTIGATION_LICENSE_SUCCESS); + }, + updateInvestigationLicenseFailure: ({ commit }, error: Error) => { + commit(MutationTypes.UPDATE_INVESTIGATION_LICENSE_FAILURE, error); + }, // DELETE USER PRIVILEGE deletePrivilegeRequest: async ({ commit, dispatch }, { compact, @@ -290,6 +350,66 @@ export default { unencumberPrivilegeFailure: ({ commit }, error: Error) => { commit(MutationTypes.UNENCUMBER_PRIVILEGE_FAILURE, error); }, + // CREATE INVESTIGATION FOR USER PRIVILEGE + createInvestigationPrivilegeRequest: async ({ commit, dispatch }, { + compact, + licenseeId, + licenseState, + licenseType, + }: any) => { + commit(MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_REQUEST); + return dataApi.createPrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ).then(async (response) => { + dispatch('createInvestigationPrivilegeSuccess'); + + return response; + }).catch((error) => { + dispatch('createInvestigationPrivilegeFailure', error); + throw error; + }); + }, + createInvestigationPrivilegeSuccess: ({ commit }) => { + commit(MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_SUCCESS); + }, + createInvestigationPrivilegeFailure: ({ commit }, error: Error) => { + commit(MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_FAILURE, error); + }, + // UPDATE INVESTIGATION FOR USER PRIVILEGE + updateInvestigationPrivilegeRequest: async ({ commit, dispatch }, { + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + }: any) => { + commit(MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_REQUEST); + return dataApi.updatePrivilegeInvestigation( + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + ).then(async (response) => { + dispatch('updateInvestigationPrivilegeSuccess'); + + return response; + }).catch((error) => { + dispatch('updateInvestigationPrivilegeFailure', error); + throw error; + }); + }, + updateInvestigationPrivilegeSuccess: ({ commit }) => { + commit(MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_SUCCESS); + }, + updateInvestigationPrivilegeFailure: ({ commit }, error: Error) => { + commit(MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_FAILURE, error); + }, // SET THE STORE STATE setStoreUsersPrevLastKey: ({ commit }, prevLastKey) => { commit(MutationTypes.STORE_UPDATE_PREVLASTKEY, prevLastKey); diff --git a/webroot/src/store/users/users.mutations.ts b/webroot/src/store/users/users.mutations.ts index 7fcbdc89c..d77dfd416 100644 --- a/webroot/src/store/users/users.mutations.ts +++ b/webroot/src/store/users/users.mutations.ts @@ -34,6 +34,12 @@ export enum MutationTypes { UNENCUMBER_LICENSE_REQUEST = '[Users] Unencumber License Request', UNENCUMBER_LICENSE_FAILURE = '[Users] Unencumber License Failure', UNENCUMBER_LICENSE_SUCCESS = '[Users] Unencumber License Success', + CREATE_INVESTIGATION_LICENSE_REQUEST = '[Users] Create Investigation License Request', + CREATE_INVESTIGATION_LICENSE_FAILURE = '[Users] Create Investigation License Failure', + CREATE_INVESTIGATION_LICENSE_SUCCESS = '[Users] Create Investigation License Success', + UPDATE_INVESTIGATION_LICENSE_REQUEST = '[Users] Update Investigation License Request', + UPDATE_INVESTIGATION_LICENSE_FAILURE = '[Users] Update Investigation License Failure', + UPDATE_INVESTIGATION_LICENSE_SUCCESS = '[Users] Update Investigation License Success', DELETE_PRIVILEGE_REQUEST = '[Users] Delete Privilege Request', DELETE_PRIVILEGE_FAILURE = '[Users] Delete Privilege Failure', DELETE_PRIVILEGE_SUCCESS = '[Users] Delete Privilege Success', @@ -43,6 +49,12 @@ export enum MutationTypes { UNENCUMBER_PRIVILEGE_REQUEST = '[Users] Unencumber Privilege Request', UNENCUMBER_PRIVILEGE_FAILURE = '[Users] Unencumber Privilege Failure', UNENCUMBER_PRIVILEGE_SUCCESS = '[Users] Unencumber Privilege Success', + CREATE_INVESTIGATION_PRIVILEGE_REQUEST = '[Users] Create Investigation Privilege Request', + CREATE_INVESTIGATION_PRIVILEGE_FAILURE = '[Users] Create Investigation Privilege Failure', + CREATE_INVESTIGATION_PRIVILEGE_SUCCESS = '[Users] Create Investigation Privilege Success', + UPDATE_INVESTIGATION_PRIVILEGE_REQUEST = '[Users] Update Investigation Privilege Request', + UPDATE_INVESTIGATION_PRIVILEGE_FAILURE = '[Users] Update Investigation Privilege Failure', + UPDATE_INVESTIGATION_PRIVILEGE_SUCCESS = '[Users] Update Investigation Privilege Success', STORE_UPDATE_USER = '[Users] Updated User in store', STORE_REMOVE_USER = '[Users] Remove User from store', STORE_RESET_USERS = '[Users] Reset users store', @@ -157,6 +169,30 @@ export default { state.isLoading = false; state.error = null; }, + [MutationTypes.CREATE_INVESTIGATION_LICENSE_REQUEST]: (state: any) => { + state.isLoading = true; + state.error = null; + }, + [MutationTypes.CREATE_INVESTIGATION_LICENSE_FAILURE]: (state: any, error: Error) => { + state.isLoading = false; + state.error = error; + }, + [MutationTypes.CREATE_INVESTIGATION_LICENSE_SUCCESS]: (state: any) => { + state.isLoading = false; + state.error = null; + }, + [MutationTypes.UPDATE_INVESTIGATION_LICENSE_REQUEST]: (state: any) => { + state.isLoading = true; + state.error = null; + }, + [MutationTypes.UPDATE_INVESTIGATION_LICENSE_FAILURE]: (state: any, error: Error) => { + state.isLoading = false; + state.error = error; + }, + [MutationTypes.UPDATE_INVESTIGATION_LICENSE_SUCCESS]: (state: any) => { + state.isLoading = false; + state.error = null; + }, [MutationTypes.DELETE_PRIVILEGE_REQUEST]: (state: any) => { state.isLoading = true; state.error = null; @@ -193,6 +229,30 @@ export default { state.isLoading = false; state.error = null; }, + [MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_REQUEST]: (state: any) => { + state.isLoading = true; + state.error = null; + }, + [MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_FAILURE]: (state: any, error: Error) => { + state.isLoading = false; + state.error = error; + }, + [MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_SUCCESS]: (state: any) => { + state.isLoading = false; + state.error = null; + }, + [MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_REQUEST]: (state: any) => { + state.isLoading = true; + state.error = null; + }, + [MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_FAILURE]: (state: any, error: Error) => { + state.isLoading = false; + state.error = error; + }, + [MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_SUCCESS]: (state: any) => { + state.isLoading = false; + state.error = null; + }, [MutationTypes.STORE_UPDATE_USER]: (state: any, user: any) => { if (user.id) { // Don't put objects with NULL IDs in the store if (state.model && state.model.length) { diff --git a/webroot/src/store/users/users.spec.ts b/webroot/src/store/users/users.spec.ts index 68e89805d..78c40064f 100644 --- a/webroot/src/store/users/users.spec.ts +++ b/webroot/src/store/users/users.spec.ts @@ -246,6 +246,56 @@ describe('Users Store Mutations', () => { expect(state.isLoading).to.equal(false); expect(state.error).to.equal(null); }); + it('should successfully create license investigation request', () => { + const state = {}; + + mutations[MutationTypes.CREATE_INVESTIGATION_LICENSE_REQUEST](state); + + expect(state.isLoading).to.equal(true); + expect(state.error).to.equal(null); + }); + it('should successfully create license investigation failure', () => { + const state = {}; + const error = new Error(); + + mutations[MutationTypes.CREATE_INVESTIGATION_LICENSE_FAILURE](state, error); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(error); + }); + it('should successfully create license investigation success', () => { + const state = {}; + + mutations[MutationTypes.CREATE_INVESTIGATION_LICENSE_SUCCESS](state); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(null); + }); + it('should successfully update license investigation request', () => { + const state = {}; + + mutations[MutationTypes.UPDATE_INVESTIGATION_LICENSE_REQUEST](state); + + expect(state.isLoading).to.equal(true); + expect(state.error).to.equal(null); + }); + it('should successfully update license investigation failure', () => { + const state = {}; + const error = new Error(); + + mutations[MutationTypes.UPDATE_INVESTIGATION_LICENSE_FAILURE](state, error); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(error); + }); + it('should successfully update license investigation success', () => { + const state = {}; + + mutations[MutationTypes.UPDATE_INVESTIGATION_LICENSE_SUCCESS](state); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(null); + }); it('should successfully delete privilege request', () => { const state = {}; @@ -321,6 +371,56 @@ describe('Users Store Mutations', () => { expect(state.isLoading).to.equal(false); expect(state.error).to.equal(null); }); + it('should successfully create privilege investigation request', () => { + const state = {}; + + mutations[MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_REQUEST](state); + + expect(state.isLoading).to.equal(true); + expect(state.error).to.equal(null); + }); + it('should successfully create privilege investigation failure', () => { + const state = {}; + const error = new Error(); + + mutations[MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_FAILURE](state, error); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(error); + }); + it('should successfully create privilege investigation success', () => { + const state = {}; + + mutations[MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_SUCCESS](state); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(null); + }); + it('should successfully update privilege investigation request', () => { + const state = {}; + + mutations[MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_REQUEST](state); + + expect(state.isLoading).to.equal(true); + expect(state.error).to.equal(null); + }); + it('should successfully update privilege investigation failure', () => { + const state = {}; + const error = new Error(); + + mutations[MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_FAILURE](state, error); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(error); + }); + it('should successfully update privilege investigation success', () => { + const state = {}; + + mutations[MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_SUCCESS](state); + + expect(state.isLoading).to.equal(false); + expect(state.error).to.equal(null); + }); it('should successfully update user (missing id)', () => { const state = {}; const user = {}; @@ -720,6 +820,100 @@ describe('Users Store Actions', async () => { expect(commit.calledOnce).to.equal(true); expect(commit.firstCall.args).to.matchPattern([MutationTypes.UNENCUMBER_LICENSE_SUCCESS]); }); + it('should successfully start create license investigation request', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + const compact = 'aslp'; + const licenseeId = '1'; + const licenseState = 'co'; + const licenseType = 'test'; + + await actions.createInvestigationLicenseRequest({ commit, dispatch }, { + compact, licenseeId, licenseState, licenseType + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_LICENSE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + }); + it('should successfully start create license investigation request (intentional error)', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + + await actions.createInvestigationLicenseRequest({ commit, dispatch }, {}).catch((error) => { + expect(error).to.be.an('error').with.property('message', 'failed license investigation create'); + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_LICENSE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + expect(dispatch.firstCall.args[0]).to.equal('createInvestigationLicenseFailure'); + }); + it('should successfully start create license investigation failure', () => { + const commit = sinon.spy(); + const error = new Error(); + + actions.createInvestigationLicenseFailure({ commit }, error); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_LICENSE_FAILURE, error]); + }); + it('should successfully start create license investigation success', () => { + const commit = sinon.spy(); + + actions.createInvestigationLicenseSuccess({ commit }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_LICENSE_SUCCESS]); + }); + it('should successfully start update license investigation request', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + const compact = 'aslp'; + const licenseeId = '1'; + const licenseState = 'co'; + const licenseType = 'test'; + const investigationId = '1'; + const encumbrance = {}; + + await actions.updateInvestigationLicenseRequest({ commit, dispatch }, { + compact, licenseeId, licenseState, licenseType, investigationId, encumbrance + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_LICENSE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + }); + it('should successfully start update license investigation request (intentional error)', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + + await actions.updateInvestigationLicenseRequest({ commit, dispatch }, {}).catch((error) => { + expect(error).to.be.an('error').with.property('message', 'failed license investigation update'); + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_LICENSE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + expect(dispatch.firstCall.args[0]).to.equal('updateInvestigationLicenseFailure'); + }); + it('should successfully start update license investigation failure', () => { + const commit = sinon.spy(); + const error = new Error(); + + actions.updateInvestigationLicenseFailure({ commit }, error); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_LICENSE_FAILURE, error]); + }); + it('should successfully start update license investigation success', () => { + const commit = sinon.spy(); + + actions.updateInvestigationLicenseSuccess({ commit }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_LICENSE_SUCCESS]); + }); it('should successfully start delete-privilege request', async () => { const commit = sinon.spy(); const dispatch = sinon.spy(); @@ -863,6 +1057,100 @@ describe('Users Store Actions', async () => { expect(commit.calledOnce).to.equal(true); expect(commit.firstCall.args).to.matchPattern([MutationTypes.UNENCUMBER_PRIVILEGE_SUCCESS]); }); + it('should successfully start create privilege investigation request', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + const compact = 'aslp'; + const licenseeId = '1'; + const licenseState = 'co'; + const licenseType = 'test'; + + await actions.createInvestigationPrivilegeRequest({ commit, dispatch }, { + compact, licenseeId, licenseState, licenseType + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + }); + it('should successfully start create privilege investigation request (intentional error)', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + + await actions.createInvestigationPrivilegeRequest({ commit, dispatch }, {}).catch((error) => { + expect(error).to.be.an('error').with.property('message', 'failed privilege investigation create'); + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + expect(dispatch.firstCall.args[0]).to.equal('createInvestigationPrivilegeFailure'); + }); + it('should successfully start create privilege investigation failure', () => { + const commit = sinon.spy(); + const error = new Error(); + + actions.createInvestigationPrivilegeFailure({ commit }, error); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_FAILURE, error]); + }); + it('should successfully start create privilege investigation success', () => { + const commit = sinon.spy(); + + actions.createInvestigationPrivilegeSuccess({ commit }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.CREATE_INVESTIGATION_PRIVILEGE_SUCCESS]); + }); + it('should successfully start update privilege investigation request', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + const compact = 'aslp'; + const licenseeId = '1'; + const licenseState = 'co'; + const licenseType = 'test'; + const investigationId = '1'; + const encumbrance = {}; + + await actions.updateInvestigationPrivilegeRequest({ commit, dispatch }, { + compact, licenseeId, licenseState, licenseType, investigationId, encumbrance + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + }); + it('should successfully start update privilege investigation request (intentional error)', async () => { + const commit = sinon.spy(); + const dispatch = sinon.spy(); + + await actions.updateInvestigationPrivilegeRequest({ commit, dispatch }, {}).catch((error) => { + expect(error).to.be.an('error').with.property('message', 'failed privilege investigation update'); + }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_REQUEST]); + expect(dispatch.calledOnce).to.equal(true); + expect(dispatch.firstCall.args[0]).to.equal('updateInvestigationPrivilegeFailure'); + }); + it('should successfully start update privilege investigation failure', () => { + const commit = sinon.spy(); + const error = new Error(); + + actions.updateInvestigationPrivilegeFailure({ commit }, error); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_FAILURE, error]); + }); + it('should successfully start update privilege investigation success', () => { + const commit = sinon.spy(); + + actions.updateInvestigationPrivilegeSuccess({ commit }); + + expect(commit.calledOnce).to.equal(true); + expect(commit.firstCall.args).to.matchPattern([MutationTypes.UPDATE_INVESTIGATION_PRIVILEGE_SUCCESS]); + }); it('should successfully set user', () => { const commit = sinon.spy(); const user = { id: '1' }; From 86f8266544bfbd3e5038c0fae631017aa8052071 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Tue, 28 Oct 2025 14:06:50 -0600 Subject: [PATCH 03/14] WIP: Investigative info - Staff licensee detail page banner - @todo: Admin workflows - @todo: Work with design on minor figma updates - @todo: Test once backend is ready --- .../src/components/LicenseCard/LicenseCard.ts | 14 ++++- .../components/PrivilegeCard/PrivilegeCard.ts | 17 +++++- webroot/src/locales/en.json | 4 ++ webroot/src/locales/es.json | 4 ++ webroot/src/network/mocks/mock.data.ts | 56 +++++++++++++++++++ .../LicensingDetail/LicensingDetail.less | 16 ++++++ .../pages/LicensingDetail/LicensingDetail.ts | 21 +++++++ .../pages/LicensingDetail/LicensingDetail.vue | 4 ++ webroot/src/styles.common/_colors.less | 6 ++ 9 files changed, 140 insertions(+), 2 deletions(-) diff --git a/webroot/src/components/LicenseCard/LicenseCard.ts b/webroot/src/components/LicenseCard/LicenseCard.ts index c2e62a72e..f8cec401c 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -217,8 +217,20 @@ class LicenseCard extends mixins(MixinForm) { return this.license?.isEncumbered() || false; } + get isUnderInvestigation(): boolean { + return this.license?.isUnderInvestigation() || false; + } + get disciplineContent(): string { - return (this.isEncumbered) ? this.$t('licensing.encumbered') : this.$t('licensing.noDiscipline'); + let content = this.$t('licensing.noDiscipline'); + + if (this.isEncumbered) { + content = this.$t('licensing.encumbered'); + } else if (this.isUnderInvestigation) { + content = this.$t('licensing.underInvestigationStatus'); + } + + return content; } get adverseActions(): Array { diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index b29d1b99e..36d3c95f9 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -68,8 +68,11 @@ class PrivilegeCard extends mixins(MixinForm) { isEncumberPrivilegeModalSuccess = false; isUnencumberPrivilegeModalDisplayed = false; isUnencumberPrivilegeModalSuccess = false; + isInvestigationLicenseModalDisplayed = false; + isInvestigationLicenseModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + selectedInvestigationId: string | null = null; modalErrorMessage = ''; // @@ -186,8 +189,20 @@ class PrivilegeCard extends mixins(MixinForm) { return this.privilege?.isEncumbered() || false; } + get isUnderInvestigation(): boolean { + return this.privilege?.isUnderInvestigation() || false; + } + get disciplineContent(): string { - return (this.isEncumbered) ? this.$t('licensing.encumbered') : this.$t('licensing.noDiscipline'); + let content = this.$t('licensing.noDiscipline'); + + if (this.isEncumbered) { + content = this.$t('licensing.encumbered'); + } else if (this.isUnderInvestigation) { + content = this.$t('licensing.underInvestigationStatus'); + } + + return content; } get adverseActions(): Array { diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index cf8eac659..83d5304c1 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -687,6 +687,10 @@ "confirmPrivilegeUnencumberSubmit": "Confirm removal(s)", "confirmPrivilegeUnencumberSuccess": "These encumbrances are set to be removed.", "confirmPrivilegeUnencumberSuccessEndDate": "End date", + "underInvestigationStatus": "Investigation", + "underInvestigationAlert1": "This practitioner is under investigation in {locations}.", + "underInvestigationAlert1MultipleLocations": "multiple states", + "underInvestigationAlert2": "Privileges can still be used while under investigation.", "expiringIn": "Expiring in", "events": "Events", "expirationTimeExplanation": "Privilege dates use the UTC-4 time zone. This means that privileges will expire at 11:59PM US Eastern Time during Daylight Savings (summer), and at 10:59 PM in the US Eastern time zone during Standard Time (winter).", diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index ddfcce1a4..90aea2aa5 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -671,6 +671,10 @@ "confirmPrivilegeUnencumberSubmit": "Confirmar eliminación(es)", "confirmPrivilegeUnencumberSuccess": "Está previsto que estos gravámenes se eliminen.", "confirmPrivilegeUnencumberSuccessEndDate": "Fecha de finalización", + "underInvestigationStatus": "Investigación", + "underInvestigationAlert1": "Este practicante está bajo investigación en {locations}.", + "underInvestigationAlert1MultipleLocations": "múltiples estados", + "underInvestigationAlert2": "Los privilegios aún se pueden utilizar mientras se está bajo investigación.", "expiringIn": "Expirando en", "events": "Eventos", "expirationTimeExplanation": "Las fechas privilegiadas utilizan la zona horaria UTC-4. Esto significa que los privilegios expirarán a las 11:59 p. m. hora del este de EE. UU. durante el horario de verano (verano) y a las 10:59 p. m. en la zona horaria del este de EE. UU. durante la hora estándar (invierno)", diff --git a/webroot/src/network/mocks/mock.data.ts b/webroot/src/network/mocks/mock.data.ts index a3d44692a..6b977a259 100644 --- a/webroot/src/network/mocks/mock.data.ts +++ b/webroot/src/network/mocks/mock.data.ts @@ -911,6 +911,35 @@ export const licensees = { }, ], }, + { + privilegeId: 'OTA-WY-1', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + compactTransactionId: '120060086502', + type: 'privilege', + jurisdiction: 'wy', + licenseJurisdiction: 'co', + licenseType: 'occupational therapy assistant', + persistedStatus: 'active', + status: 'active', + dateOfIssuance: '2024-03-19T21:30:27+00:00', + dateOfUpdate: '2025-03-26T15:56:58+00:00', + dateOfRenewal: moment().subtract(11, 'months').format(serverDateFormat), + dateOfExpiration: moment().add(1, 'month').format(serverDateFormat), + attestations: attestationResponses.map((response) => ({ ...response })), + investigations: [ + { + investigationId: '12345-ABC', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + dateOfUpdate: null, + }, + ], + }, ], }, { @@ -1836,6 +1865,33 @@ export const mockPrivilegeHistoryResponses = [ } ] }, + { + // ================================================================ + // JANET DOE (WY OTA) + // ================================================================ + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + privilegeId: 'OTA-WY-1', + events: [ + { + type: 'privilegeUpdate', + updateType: 'issuance', + dateOfUpdate: '2024-03-19T21:30:27+00:00', + effectiveDate: '2024-03-19T21:30:27+00:00', + createDate: '2024-03-19T21:30:27+00:00' + }, + { + type: 'privilegeUpdate', + updateType: 'investigation', + dateOfUpdate: moment().subtract(1, 'week').format(serverDatetimeFormat), + effectiveDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + createDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + note: '', + } + ] + }, { // ================================================================ // TYLER DURDEN (AL OTA) diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.less b/webroot/src/pages/LicensingDetail/LicensingDetail.less index a08d179bd..9b92daf25 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.less +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.less @@ -10,6 +10,22 @@ @spacingTablet: 4.8rem; @spacingDesktop: 12rem; + .licensee-alert { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 0.8rem 2rem; + font-weight: @fontWeightBold; + background-color: @midYellow; + + .alert-icon { + height: 1.8rem; + margin-right: 0.8rem; + stroke: @fontColor; + } + } + .title-row { display: flex; flex-direction: row; diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.ts b/webroot/src/pages/LicensingDetail/LicensingDetail.ts index 156c9985d..483a2bedd 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.ts +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.ts @@ -12,6 +12,7 @@ import LicenseCard from '@/components/LicenseCard/LicenseCard.vue'; import PrivilegeCard from '@/components/PrivilegeCard/PrivilegeCard.vue'; import MilitaryAffiliationInfoBlock from '@components/MilitaryAffiliationInfoBlock/MilitaryAffiliationInfoBlock.vue'; import CollapseCaretButton from '@components/CollapseCaretButton/CollapseCaretButton.vue'; +import AlertIcon from '@components/Icons/AlertTriangle/AlertTriangle.vue'; import LicenseIcon from '@components/Icons/LicenseIcon/LicenseIcon.vue'; import ExpirationExplanationIcon from '@components/Icons/ExpirationExplanationIcon/ExpirationExplanationIcon.vue'; import { CompactType } from '@models/Compact/Compact.model'; @@ -28,6 +29,7 @@ import { dataApi } from '@network/data.api'; LicenseCard, PrivilegeCard, CollapseCaretButton, + AlertIcon, LicenseIcon, MilitaryAffiliationInfoBlock, ExpirationExplanationIcon @@ -106,6 +108,25 @@ export default class LicensingDetail extends Vue { return storeRecord; } + get isLicenseeUnderInvestigation(): boolean { + return this.licensee?.isUnderInvestigation() || false; + } + + get licenseeInvestigationAlertContent(): string { + const investigationStates = this.licensee?.underInvestigationStates() || []; + const statesContent = (investigationStates.length === 1) + ? investigationStates[0].name() + : this.$t('licensing.underInvestigationAlert1MultipleLocations'); + let alertContent = ''; + + if (investigationStates.length) { + alertContent += `${this.$t('licensing.underInvestigationAlert1', { locations: statesContent })} + ${this.$t('licensing.underInvestigationAlert2')}`; + } + + return alertContent; + } + get licenseeNameDisplay(): string { return this.licensee?.nameDisplay() || ''; } diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.vue b/webroot/src/pages/LicensingDetail/LicensingDetail.vue index 04f5ae847..1d401b90a 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.vue +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.vue @@ -11,6 +11,10 @@
+
+ + {{ licenseeInvestigationAlertContent }} +
@@ -325,6 +345,77 @@
+ + +
diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.less b/webroot/src/components/PrivilegeCard/PrivilegeCard.less index 8db594b95..220d5c4af 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.less +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.less @@ -80,7 +80,7 @@ z-index: 1; display: flex; flex-direction: column; - min-width: 16rem; + min-width: 18rem; padding: 0.6rem 0.8rem; border-radius: 12px; background-color: @white; @@ -109,6 +109,22 @@ font-weight: @fontWeight; cursor: default; } + + &.new-section { + margin-top: 0.4rem; + padding-top: 1rem; + + &::before { + position: absolute; + top: 0; + left: 0.8rem; + width: calc(100% - 1.6rem); + height: 1px; + margin-bottom: 0.4rem; + background-color: @lightGrey; + content: ''; + } + } } } } @@ -165,7 +181,7 @@ :deep(.modal-container) { width: 95%; - max-width: 60rem; + max-width: 62rem; padding: 2rem; @media @tabletWidth { @@ -178,6 +194,15 @@ .form-row { margin-bottom: 1.6rem; + + &.static-container { + display: flex; + flex-direction: column; + + @media @desktopWidth { + flex-direction: row; + } + } } #notes { @@ -185,17 +210,23 @@ border-color: @fontColor; } - .encumber-privilege-form-input-container { + .encumber-privilege-form-input-container, + .add-investigation-form-input-container { padding: 1.6rem; border-radius: 8px; background-color: @veryLightGrey; } - .unencumber-row { + .unencumber-row, + .end-investigation-row { display: flex; flex-direction: column; margin-bottom: 3.2rem; + &.end-investigation-row { + margin-bottom: 1.2rem; + } + @media @desktopWidth { flex-direction: row; align-items: flex-end; @@ -206,15 +237,30 @@ margin: 0; } - .unencumber-select { + .unencumber-select, + .end-investigation-select { display: flex; flex-direction: column; - margin-bottom: 0.4rem; padding: 1rem 0.6rem; border: 1px solid @midGrey; border-radius: 12px; cursor: pointer; + &.end-investigation-select { + width: 100%; + padding-top: 1.4rem; + padding-bottom: 1rem; + } + + &.unencumber-select { + margin-bottom: 0.4rem; + + @media @desktopWidth { + width: 65%; + margin-bottom: 0; + } + } + &.selected { border-color: @fontColor; background-color: @veryLightBlue; @@ -225,11 +271,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -241,7 +282,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -257,13 +299,34 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .static-input { + display: flex; + flex-direction: column; + width: 100%; + + &:not(:last-child) { + margin-bottom: 1.2rem; + } + + @media @desktopWidth { + width: 50%; + } + + .static-label { + margin-bottom: 0.2rem; + font-weight: @fontWeightBold; + font-size: @fontSize; + } + + .static-value { + overflow: hidden; + font-size: @fontSize; + text-overflow: ellipsis; + } } - .static-value { - font-size: 1.7rem; + .info-block { + margin-bottom: 2rem; } .form-field-error { diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index 36d3c95f9..1a1fda61c 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -35,6 +35,7 @@ import { Compact } from '@models/Compact/Compact.model'; import { State } from '@/models/State/State.model'; import { StaffUser, CompactPermission } from '@models/StaffUser/StaffUser.model'; import { AdverseAction } from '@/models/AdverseAction/AdverseAction.model'; +import { Investigation } from '@/models/Investigation/Investigation.model'; import { FormInput } from '@/models/FormInput/FormInput.model'; import Joi from 'joi'; import moment from 'moment'; @@ -68,11 +69,15 @@ class PrivilegeCard extends mixins(MixinForm) { isEncumberPrivilegeModalSuccess = false; isUnencumberPrivilegeModalDisplayed = false; isUnencumberPrivilegeModalSuccess = false; - isInvestigationLicenseModalDisplayed = false; - isInvestigationLicenseModalSuccess = false; + isAddInvestigationModalDisplayed = false; + isAddInvestigationModalSuccess = false; + isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; + isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; - selectedInvestigationId: string | null = null; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -209,6 +214,10 @@ class PrivilegeCard extends mixins(MixinForm) { return this.privilege?.adverseActions || []; } + get investigations(): Array { + return this.privilege?.investigations || []; + } + get encumberDisciplineOptions(): Array<{ value: string, name: string | ComputedRef }> { const options = this.$tm('licensing.disciplineTypes').map((disciplineType) => ({ value: disciplineType.key, @@ -239,10 +248,26 @@ class PrivilegeCard extends mixins(MixinForm) { return options; } + get endInvestigationModalTitle(): string { + let modalTitle = this.$t('licensing.confirmLicenseInvestigationEndSelectTitle'); + + if (this.isEndInvestigationModalSuccess) { + modalTitle = ' '; + } else if (this.isEndInvestigationModalConfirm) { + modalTitle = this.$t('licensing.confirmLicenseInvestigationEndTitle'); + } + + return modalTitle; + } + get isUnencumberSubmitEnabled(): boolean { return Boolean(this.isFormValid && !this.isFormLoading && this.selectedEncumbrances.length); } + get isEndInvestigationSubmitEnabled(): boolean { + return Boolean(this.isFormValid && !this.isFormLoading && this.selectedInvestigation); + } + get isMockPopulateEnabled(): boolean { return Boolean(this.$envConfig.isDevelopment); } @@ -257,6 +282,10 @@ class PrivilegeCard extends mixins(MixinForm) { this.initFormInputsEncumberPrivilege(); } else if (this.isUnencumberPrivilegeModalDisplayed) { this.initFormInputsUnencumberPrivilege(); + } else if (this.isAddInvestigationModalDisplayed) { + this.initFormInputsAddInvestigation(); + } else if (this.isEndInvestigationModalDisplayed) { + this.initFormInputsEndInvestigation(); } } @@ -348,6 +377,38 @@ class PrivilegeCard extends mixins(MixinForm) { }); } + initFormInputsAddInvestigation(): void { + this.formData = reactive({ + addInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + this.watchFormInputs(); + } + + initFormInputsEndInvestigation(): void { + this.formData = reactive({ + endInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + + this.investigations.forEach((investigation: Investigation) => { + const investigationId = investigation.id; + const investigationInput = new FormInput({ + id: `end-investigation-data-${investigationId}`, + name: `end-investigation-data-${investigationId}`, + label: this.$t('licensing.investigationStartedOn', { date: investigation.startDateDisplay() }), + isDisabled: Boolean(investigation.updateDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -469,6 +530,7 @@ class PrivilegeCard extends mixins(MixinForm) { event?.preventDefault(); this.isEncumberPrivilegeModalDisplayed = false; this.isEncumberPrivilegeModalSuccess = false; + this.selectedInvestigation = null; } focusTrapEncumberPrivilegeModal(event: KeyboardEvent): void { @@ -602,7 +664,7 @@ class PrivilegeCard extends mixins(MixinForm) { this.validateAll(); } - getFirstEnabledFormInputId(): string { + getFirstEnabledUnencumberFormInputId(): string { const { formData } = this; const firstEnabledFormInput: string = Object.keys(formData) .filter((key) => key !== 'unencumberModalContinue') @@ -630,7 +692,7 @@ class PrivilegeCard extends mixins(MixinForm) { focusTrapUnencumberPrivilegeModal(event: KeyboardEvent): void { const { isUnencumberSubmitEnabled } = this; - const firstEnabledInputId = this.getFirstEnabledFormInputId(); + const firstEnabledInputId = this.getFirstEnabledUnencumberFormInputId(); const firstTabIndex = document.getElementById(firstEnabledInputId); const lastTabIndex = (isUnencumberSubmitEnabled) ? document.getElementById('submit-modal-continue') @@ -694,6 +756,240 @@ class PrivilegeCard extends mixins(MixinForm) { } } + // ======================================================= + // ADD INVESTIGATION + // ======================================================= + async toggleAddInvestigationModal(): Promise { + this.resetForm(); + this.isAddInvestigationModalDisplayed = !this.isAddInvestigationModalDisplayed; + + if (this.isAddInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeAddInvestigationModal(event?: Event): void { + event?.preventDefault(); + this.isAddInvestigationModalDisplayed = false; + this.isAddInvestigationModalSuccess = false; + } + + focusTrapAddInvestigationModal(event: KeyboardEvent): void { + const firstTabIndex = document.getElementById('add-investigation-modal-cancel-button'); + const lastTabIndex = (this.isFormValid && !this.isFormLoading && !this.isAddInvestigationModalSuccess) + ? document.getElementById(this.formData.addInvestigationModalContinue.id) + : document.getElementById('add-investigation-modal-cancel-button'); + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + async submitAddInvestigation(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + privilegeTypeAbbrev + } = this; + + await this.$store.dispatch(`users/createInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isAddInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + + // ======================================================= + // END INVESTIGATION + // ======================================================= + clickEndInvestigationItem(investigation: Investigation, event?: PointerEvent | KeyboardEvent): void { + const { srcElement, type } = event || {}; + const investigationId = investigation?.id; + const nodeType = (srcElement as Element)?.nodeName; + + // Handle wrapped checkbox input so that the wrapper events act the same as the nested checkbox input + if (nodeType === 'INPUT') { + if (type === 'keyup') { + event?.preventDefault(); + } + event?.stopPropagation(); + } else if (nodeType === 'LABEL') { + event?.preventDefault(); + } + + if (investigationId) { + const formInput = this.formData[`end-investigation-data-${investigationId}`]; + const existingValue = Boolean(formInput?.value); + + if (formInput) { + formInput.value = !existingValue; + + if (formInput.value) { + this.addEndInvestigationFormData(investigation); + } else { + this.removeEndInvestigationFormData(); + } + } + } + } + + async addEndInvestigationFormData(investigation: Investigation): Promise { + if (investigation) { + this.selectedInvestigation = investigation; + this.investigationInputs.forEach((input: FormInput) => { + if (input.id !== `end-investigation-data-${investigation.id}`) { + input.value = ''; + } + }); + + this.watchFormInputs(); + this.validateAll(); + } + } + + removeEndInvestigationFormData(): void { + this.selectedInvestigation = null; + this.watchFormInputs(); + this.validateAll(); + } + + getFirstEnabledEndInvestigationFormInputId(): string { + const { formData } = this; + const firstEnabledFormInput: string = Object.keys(formData) + .filter((key) => key !== 'endInvestigationModalContinue') + .find((key) => !formData[key].isDisabled) || ''; + const firstEnabledInputId = formData[firstEnabledFormInput]?.id || 'end-investigation-modal-cancel-button'; + + return firstEnabledInputId; + } + + async toggleEndInvestigationModal(): Promise { + this.resetForm(); + this.isEndInvestigationModalDisplayed = !this.isEndInvestigationModalDisplayed; + + if (this.isEndInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeEndInvestigationModal(event?: Event, keepSelectedInvestigation = false): void { + event?.preventDefault(); + + if (!keepSelectedInvestigation) { + this.selectedInvestigation = null; + } + + this.isEndInvestigationModalDisplayed = false; + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = false; + } + + focusTrapEndInvestigationModal(event: KeyboardEvent): void { + const { + isEndInvestigationModalConfirm, + isEndInvestigationModalSuccess, + isEndInvestigationSubmitEnabled + } = this; + const firstEnabledInputId = (isEndInvestigationModalConfirm || isEndInvestigationModalSuccess) + ? 'end-investigation-modal-cancel-button' + : this.getFirstEnabledEndInvestigationFormInputId(); + const firstTabIndex = document.getElementById(firstEnabledInputId); + const lastTabIndex = (isEndInvestigationSubmitEnabled && !isEndInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('end-investigation-modal-cancel-button'); + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + continueToEndInvestigationConfirm(): void { + this.isEndInvestigationModalConfirm = true; + } + + submitEndInvestigationWithEncumbrance(): void { + this.closeEndInvestigationModal(undefined, true); + this.toggleEncumberPrivilegeModal(); + } + + async submitEndInvestigationWithoutEncumbrance(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + privilegeTypeAbbrev, + } = this; + const investigationId = this.selectedInvestigation?.id; + const errorMessages: Array = []; + + await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + investigationId, + }).catch((err) => { + errorMessages.push(err?.message || this.$t('common.error')); + }); + + if (errorMessages.length) { + this.modalErrorMessage = errorMessages.join('; '); + this.isFormError = true; + } + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + async focusTrapTeleportedDatepicker(formInput: FormInput, isOpen: boolean): Promise { if (isOpen) { await nextTick(); @@ -707,6 +1003,10 @@ class PrivilegeCard extends mixins(MixinForm) { return this.selectedEncumbrances.some((selected: AdverseAction) => selected.id === adverseAction.id); } + isInvestigationSelected(investigation: Investigation): boolean { + return this.selectedInvestigation?.id === investigation.id; + } + dateDisplayFormat(unformattedDate: string): string { return dateDisplay(unformattedDate); } @@ -739,6 +1039,15 @@ class PrivilegeCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((invevstigation) => !invevstigation.hasEndDate()) + .map(async (invevstigation) => { + this.clickEndInvestigationItem(invevstigation); + await nextTick(); + })); + await nextTick(); + this.validateAll({ asTouched: true }); } } } diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue index 582a9dba5..3194c9271 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue @@ -64,6 +64,26 @@ > {{ $t('licensing.unencumber') }} +
  • + {{ $t('licensing.addInvestigation') }} +
  • +
  • + {{ $t('licensing.endInvestigation') }} +
  • @@ -171,21 +191,25 @@ :isEnabled="isMockPopulateEnabled" @selected="mockPopulate" /> -
    -
    {{ $t('licensing.practitionerName') }}
    -
    {{ licenseeName }}
    -
    -
    -
    {{ $t('common.state') }}
    -
    {{ stateContent }}
    -
    -
    -
    {{ $t('licensing.privilegeId') }}
    -
    {{ privilegeId }}
    +
    +
    +
    {{ $t('licensing.practitionerName') }}
    +
    {{ licenseeName }}
    +
    +
    +
    {{ $t('common.state') }}
    +
    {{ stateContent }}
    +
    -
    -
    {{ $t('licensing.privilegeType') }}
    -
    {{ privilegeTypeAbbrev }}
    +
    +
    +
    {{ $t('licensing.privilegeId') }}
    +
    {{ privilegeId }}
    +
    +
    +
    {{ $t('licensing.privilegeType') }}
    +
    {{ privilegeTypeAbbrev }}
    +
    @@ -385,6 +409,217 @@
    + + + + + +
    diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index 83d5304c1..d78bca4d9 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -687,6 +687,32 @@ "confirmPrivilegeUnencumberSubmit": "Confirm removal(s)", "confirmPrivilegeUnencumberSuccess": "These encumbrances are set to be removed.", "confirmPrivilegeUnencumberSuccessEndDate": "End date", + "addInvestigation": "Add investigation", + "confirmLicenseInvestigationStartTitle": "Confirm Current Significant Investigative Information", + "confirmLicenseInvestigationStartSubtext": "Clicking “Yes” will notify compact member states of a significant investigation ongoing for this practitioner license. Are you sure you want to proceed?", + "confirmLicenseInvestigationStartSubmit": "Yes, send notification", + "confirmLicenseInvestigationStartSuccess": "Member states will be notified of a significant ongoing investigation", + "confirmPrivilegeInvestigationStartTitle": "Confirm Current Significant Investigative Information", + "confirmPrivilegeInvestigationStartSubtext": "Clicking “Yes” will notify compact member states of a significant investigation ongoing for this practitioner privilege. Are you sure you want to proceed?", + "confirmPrivilegeInvestigationStartSubmit": "Yes, send notification", + "confirmPrivilegeInvestigationStartSuccess": "Member states will be notified of a significant ongoing investigation", + "endInvestigation": "End investigation", + "investigationStartedOn": "Investigation started on {date}", + "investigationEndedOn": "Ended on {date}", + "confirmLicenseInvestigationEndSelectTitle": "Select an investigation to end", + "confirmLicenseInvestigationEndTitle": "End an investigation", + "confirmLicenseInvestigationEndSubtext1": "You are ending an investigation that started on {date}.", + "confirmLicenseInvestigationEndSubtext2": "Do you need to report an encumbrance? If so, click “Add encumbrance” below. If not, click “No encumbrance” below, and other states will be notified that the investigation was ended with no encumbrance.", + "confirmLicenseInvestigationEndSubmit1": "No encumbrance", + "confirmLicenseInvestigationEndSubmit2": "Add encumbrance", + "confirmLicenseInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", + "confirmPrivilegeInvestigationEndSelectTitle": "Select an investigation to end", + "confirmPrivilegeInvestigationEndTitle": "End an investigation", + "confirmPrivilegeInvestigationEndSubtext1": "You are ending an investigation that started on {date}.", + "confirmPrivilegeInvestigationEndSubtext2": "Do you need to report an encumbrance? If so, click “Add encumbrance” below. If not, click “No encumbrance” below, and other states will be notified that the investigation was ended with no encumbrance.", + "confirmPrivilegeInvestigationEndSubmit1": "No encumbrance", + "confirmPrivilegeInvestigationEndSubmit2": "Add encumbrance", + "confirmPrivilegeInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", "underInvestigationStatus": "Investigation", "underInvestigationAlert1": "This practitioner is under investigation in {locations}.", "underInvestigationAlert1MultipleLocations": "multiple states", diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index 90aea2aa5..2df436d73 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -671,6 +671,32 @@ "confirmPrivilegeUnencumberSubmit": "Confirmar eliminación(es)", "confirmPrivilegeUnencumberSuccess": "Está previsto que estos gravámenes se eliminen.", "confirmPrivilegeUnencumberSuccessEndDate": "Fecha de finalización", + "addInvestigation": "Añadir investigación", + "confirmLicenseInvestigationStartTitle": "Confirmar la información de investigación significativa actual", + "confirmLicenseInvestigationStartSubtext": "Al hacer clic en “Sí”, se notificará a los estados miembros del acuerdo sobre una investigación importante en curso relacionada con esta licencia profesional. ¿Está seguro de que desea continuar?", + "confirmLicenseInvestigationStartSubmit": "Sí, enviar notificación", + "confirmLicenseInvestigationStartSuccess": "Los Estados miembros serán notificados de una importante investigación en curso.", + "confirmPrivilegeInvestigationStartTitle": "Confirmar la información de investigación significativa actual", + "confirmPrivilegeInvestigationStartSubtext": "Al hacer clic en “Sí”, se notificará a los Estados miembros del pacto que se está llevando a cabo una investigación importante sobre este privilegio profesional. ¿Está seguro de que desea continuar?", + "confirmPrivilegeInvestigationStartSubmit": "Sí, enviar notificación", + "confirmPrivilegeInvestigationStartSuccess": "Los Estados miembros serán notificados de una importante investigación en curso.", + "endInvestigation": "Fin de la investigación", + "investigationStartedOn": "La investigación comenzó el {date}", + "investigationEndedOn": "Finalizó el {date}", + "confirmLicenseInvestigationEndSelectTitle": "Seleccione una investigación para finalizar", + "confirmLicenseInvestigationEndTitle": "Finalizar una investigación", + "confirmLicenseInvestigationEndSubtext1": "Estás dando por finalizada una investigación que comenzó el {date}.", + "confirmLicenseInvestigationEndSubtext2": "¿Necesita reportar algún gravamen? De ser así, haga clic en “Agregar gravamen” a continuación. De lo contrario, haga clic en “Sin gravamen” a continuación, y se notificará a los demás estados que la investigación concluyó sin que se encontrara ningún gravamen.", + "confirmLicenseInvestigationEndSubmit1": "Sin gravamen", + "confirmLicenseInvestigationEndSubmit2": "Agregar gravamen", + "confirmLicenseInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", + "confirmPrivilegeInvestigationEndSelectTitle": "Seleccione una investigación para finalizar", + "confirmPrivilegeInvestigationEndTitle": "Finalizar una investigación", + "confirmPrivilegeInvestigationEndSubtext1": "Estás dando por finalizada una investigación que comenzó el {date}.", + "confirmPrivilegeInvestigationEndSubtext2": "¿Necesita reportar algún gravamen? De ser así, haga clic en “Agregar gravamen” a continuación. De lo contrario, haga clic en “Sin gravamen” a continuación, y se notificará a los demás estados que la investigación concluyó sin que se encontrara ningún gravamen.", + "confirmPrivilegeInvestigationEndSubmit1": "Sin gravamen", + "confirmPrivilegeInvestigationEndSubmit2": "Agregar gravamen", + "confirmPrivilegeInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", "underInvestigationStatus": "Investigación", "underInvestigationAlert1": "Este practicante está bajo investigación en {locations}.", "underInvestigationAlert1MultipleLocations": "múltiples estados", diff --git a/webroot/src/network/mocks/mock.data.ts b/webroot/src/network/mocks/mock.data.ts index 6b977a259..09a1bd7bb 100644 --- a/webroot/src/network/mocks/mock.data.ts +++ b/webroot/src/network/mocks/mock.data.ts @@ -85,6 +85,14 @@ export const staffAccount = { readSsn: true, }, }, + wy: { + actions: { + admin: true, + write: true, + readPrivate: true, + readSsn: true, + }, + }, }, }, aslp: { @@ -938,6 +946,26 @@ export const licensees = { creationDate: moment().subtract(1, 'week').format(serverDatetimeFormat), dateOfUpdate: null, }, + { + investigationId: '12345-DEF', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'month').format(serverDatetimeFormat), + dateOfUpdate: moment().subtract(3, 'weeks').format(serverDatetimeFormat), + }, + { + investigationId: '12345-GHI', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'year').format(serverDatetimeFormat), + dateOfUpdate: null + }, ], }, ], From 7cb1285d3171fed0b3bbc67b4d7424b812b28b69 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Thu, 30 Oct 2025 11:09:37 -0600 Subject: [PATCH 05/14] WIP: Investigative info - Start tying together with encumbrance flow - Start applying to license UI - @todo: Work with design on minor figma updates - @todo: Test once backend is ready --- .../components/LicenseCard/LicenseCard.less | 75 ++++- .../src/components/LicenseCard/LicenseCard.ts | 286 ++++++++++++++++-- .../components/LicenseCard/LicenseCard.vue | 214 +++++++++++-- .../PrivilegeCard/PrivilegeCard.less | 1 + .../components/PrivilegeCard/PrivilegeCard.ts | 78 +++-- .../PrivilegeCard/PrivilegeCard.vue | 41 ++- webroot/src/network/data.api.ts | 27 +- webroot/src/network/licenseApi/data.api.ts | 40 +-- webroot/src/network/mocks/mock.data.api.ts | 15 +- webroot/src/network/mocks/mock.data.ts | 22 ++ webroot/src/store/users/users.actions.ts | 8 +- 11 files changed, 680 insertions(+), 127 deletions(-) diff --git a/webroot/src/components/LicenseCard/LicenseCard.less b/webroot/src/components/LicenseCard/LicenseCard.less index 28b4c2eee..98c95fa24 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.less +++ b/webroot/src/components/LicenseCard/LicenseCard.less @@ -260,6 +260,16 @@ .form-row { margin-bottom: 1.6rem; + + &.static-container { + display: flex; + flex-direction: column; + + @media @desktopWidth { + flex-direction: row; + margin-bottom: 0.2rem; + } + } } #notes { @@ -274,11 +284,16 @@ background-color: @veryLightGrey; } - .unencumber-row { + .unencumber-row, + .end-investigation-row { display: flex; flex-direction: column; margin-bottom: 3.2rem; + &.end-investigation-row { + margin-bottom: 1.2rem; + } + @media @desktopWidth { flex-direction: row; align-items: flex-end; @@ -289,15 +304,30 @@ margin: 0; } - .unencumber-select { + .unencumber-select, + .end-investigation-select { display: flex; flex-direction: column; - margin-bottom: 0.4rem; padding: 1rem 0.6rem; border: 1px solid @midGrey; border-radius: 12px; cursor: pointer; + &.end-investigation-select { + width: 100%; + padding-top: 1.4rem; + padding-bottom: 1rem; + } + + &.unencumber-select { + margin-bottom: 0.4rem; + + @media @desktopWidth { + width: 65%; + margin-bottom: 0; + } + } + &.selected { border-color: @fontColor; background-color: @veryLightBlue; @@ -308,11 +338,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -324,7 +349,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -340,13 +366,34 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .static-input { + display: flex; + flex-direction: column; + width: 100%; + + &:not(:last-child) { + margin-bottom: 1.2rem; + } + + @media @desktopWidth { + width: 50%; + } + + .static-label { + margin-bottom: 0.2rem; + font-weight: @fontWeightBold; + font-size: @fontSize; + } + + .static-value { + overflow: hidden; + font-size: @fontSize; + text-overflow: ellipsis; + } } - .static-value { - font-size: 1.7rem; + .info-block { + margin-bottom: 2rem; } .form-field-error { diff --git a/webroot/src/components/LicenseCard/LicenseCard.ts b/webroot/src/components/LicenseCard/LicenseCard.ts index 7e76677f5..9aec2c672 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -77,10 +77,12 @@ class LicenseCard extends mixins(MixinForm) { isAddInvestigationModalDisplayed = false; isAddInvestigationModalSuccess = false; isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; - selectedInvestigationId: Investigation | null = null; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -240,6 +242,10 @@ class LicenseCard extends mixins(MixinForm) { return this.license?.adverseActions || []; } + get investigations(): Array { + return this.license?.investigations || []; + } + get encumberDisciplineOptions(): Array<{ value: string, name: string | ComputedRef }> { const options = this.$tm('licensing.disciplineTypes').map((disciplineType) => ({ value: disciplineType.key, @@ -270,10 +276,26 @@ class LicenseCard extends mixins(MixinForm) { return options; } + get endInvestigationModalTitle(): string { + let modalTitle = this.$t('licensing.confirmLicenseInvestigationEndSelectTitle'); + + if (this.isEndInvestigationModalSuccess) { + modalTitle = ' '; + } else if (this.isEndInvestigationModalConfirm) { + modalTitle = this.$t('licensing.confirmLicenseInvestigationEndTitle'); + } + + return modalTitle; + } + get isUnencumberSubmitEnabled(): boolean { return Boolean(this.isFormValid && !this.isFormLoading && this.selectedEncumbrances.length); } + get isEndInvestigationSubmitEnabled(): boolean { + return Boolean(this.isFormValid && !this.isFormLoading && this.selectedInvestigation); + } + get isMockPopulateEnabled(): boolean { return Boolean(this.$envConfig.isDevelopment); } @@ -288,6 +310,8 @@ class LicenseCard extends mixins(MixinForm) { this.initFormInputsUnencumberLicense(); } else if (this.isAddInvestigationModalDisplayed) { this.initFormInputsAddInvestigation(); + } else if (this.isEndInvestigationModalDisplayed) { + this.initFormInputsEndInvestigation(); } } @@ -371,6 +395,28 @@ class LicenseCard extends mixins(MixinForm) { this.watchFormInputs(); } + initFormInputsEndInvestigation(): void { + this.formData = reactive({ + endInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + + this.investigations.forEach((investigation: Investigation) => { + const investigationId = investigation.id; + const investigationInput = new FormInput({ + id: `end-investigation-data-${investigationId}`, + name: `end-investigation-data-${investigationId}`, + label: this.$t('licensing.investigationStartedOn', { date: investigation.startDateDisplay() }), + isDisabled: Boolean(investigation.updateDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -465,25 +511,54 @@ class LicenseCard extends mixins(MixinForm) { licenseTypeAbbrev } = this; - await this.$store.dispatch(`users/encumberLicenseRequest`, { - compact: compactType, - licenseeId, - licenseState: stateAbbrev, - licenseType: licenseTypeAbbrev.toLowerCase(), - encumbranceType: this.formData.encumberModalDisciplineAction.value, - ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) - ? { - npdbCategories: this.formData.encumberModalNpdbCategories.value, - } - : { - npdbCategory: this.formData.encumberModalNpdbCategory.value, - } - ), - startDate: this.formData.encumberModalStartDate.value, - }).catch((err) => { - this.modalErrorMessage = err?.message || this.$t('common.error'); - this.isFormError = true; - }); + if (this.selectedInvestigation) { + // Submit the encumbrance as part of a selected investigation update + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + investigationId, + encumbrance: { + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } else { + // Submit the encumbrance on its own + await this.$store.dispatch(`users/encumberLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } if (!this.isFormError) { this.isFormSuccessful = true; @@ -725,6 +800,164 @@ class LicenseCard extends mixins(MixinForm) { } } + // ======================================================= + // END INVESTIGATION + // ======================================================= + clickEndInvestigationItem(investigation: Investigation, event?: PointerEvent | KeyboardEvent): void { + const { srcElement, type } = event || {}; + const investigationId = investigation?.id; + const nodeType = (srcElement as Element)?.nodeName; + + // Handle wrapped checkbox input so that the wrapper events act the same as the nested checkbox input + if (nodeType === 'INPUT') { + if (type === 'keyup') { + event?.preventDefault(); + } + event?.stopPropagation(); + } else if (nodeType === 'LABEL') { + event?.preventDefault(); + } + + if (investigationId) { + const formInput = this.formData[`end-investigation-data-${investigationId}`]; + const existingValue = Boolean(formInput?.value); + + if (formInput) { + formInput.value = !existingValue; + + if (formInput.value) { + this.addEndInvestigationFormData(investigation); + } else { + this.removeEndInvestigationFormData(); + } + } + } + } + + async addEndInvestigationFormData(investigation: Investigation): Promise { + if (investigation) { + this.selectedInvestigation = investigation; + this.investigationInputs.forEach((input: FormInput) => { + if (input.id !== `end-investigation-data-${investigation.id}`) { + input.value = ''; + } + }); + + this.watchFormInputs(); + this.validateAll(); + } + } + + removeEndInvestigationFormData(): void { + this.selectedInvestigation = null; + this.watchFormInputs(); + this.validateAll(); + } + + getFirstEnabledEndInvestigationFormInputId(): string { + const { formData } = this; + const firstEnabledFormInput: string = Object.keys(formData) + .filter((key) => key !== 'endInvestigationModalContinue') + .find((key) => !formData[key].isDisabled) || ''; + const firstEnabledInputId = formData[firstEnabledFormInput]?.id || 'end-investigation-modal-cancel-button'; + + return firstEnabledInputId; + } + + async toggleEndInvestigationModal(): Promise { + this.resetForm(); + this.isEndInvestigationModalDisplayed = !this.isEndInvestigationModalDisplayed; + + if (this.isEndInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeEndInvestigationModal(event?: Event, keepSelectedInvestigation = false): void { + event?.preventDefault(); + + if (!keepSelectedInvestigation) { + this.selectedInvestigation = null; + } + + this.isEndInvestigationModalDisplayed = false; + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = false; + } + + focusTrapEndInvestigationModal(event: KeyboardEvent): void { + const { + isEndInvestigationModalConfirm, + isEndInvestigationModalSuccess, + isEndInvestigationSubmitEnabled + } = this; + const firstEnabledInputId = (isEndInvestigationModalConfirm || isEndInvestigationModalSuccess) + ? 'end-investigation-modal-cancel-button' + : this.getFirstEnabledEndInvestigationFormInputId(); + const firstTabIndex = document.getElementById(firstEnabledInputId); + const lastTabIndex = (isEndInvestigationSubmitEnabled && !isEndInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('end-investigation-modal-cancel-button'); + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + continueToEndInvestigationConfirm(): void { + this.isEndInvestigationModalConfirm = true; + } + + submitEndInvestigationWithEncumbrance(): void { + this.closeEndInvestigationModal(undefined, true); + this.toggleEncumberLicenseModal(); + } + + async submitEndInvestigationWithoutEncumbrance(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + licenseTypeAbbrev, + } = this; + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + investigationId, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + async focusTrapTeleportedDatepicker(formInput: FormInput, isOpen: boolean): Promise { if (isOpen) { await nextTick(); @@ -738,6 +971,10 @@ class LicenseCard extends mixins(MixinForm) { return this.selectedEncumbrances.some((selected: AdverseAction) => selected.id === adverseAction.id); } + isInvestigationSelected(investigation: Investigation): boolean { + return this.selectedInvestigation?.id === investigation.id; + } + dateDisplayFormat(unformattedDate: string): string { return dateDisplay(unformattedDate); } @@ -770,6 +1007,15 @@ class LicenseCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((invevstigation) => !invevstigation.hasEndDate()) + .map(async (invevstigation) => { + this.clickEndInvestigationItem(invevstigation); + await nextTick(); + })); + await nextTick(); + this.validateAll({ asTouched: true }); } } } diff --git a/webroot/src/components/LicenseCard/LicenseCard.vue b/webroot/src/components/LicenseCard/LicenseCard.vue index 0d5f3cd11..54f5e28bc 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.vue +++ b/webroot/src/components/LicenseCard/LicenseCard.vue @@ -131,21 +131,25 @@ :isEnabled="isMockPopulateEnabled" @selected="mockPopulate" /> -
    -
    {{ $t('licensing.practitionerName') }}
    -
    {{ licenseeName }}
    -
    -
    -
    {{ $t('common.state') }}
    -
    {{ stateContent }}
    -
    -
    -
    {{ $t('licensing.licenseNumber') }}
    -
    {{ licenseNumber }}
    +
    +
    +
    {{ $t('licensing.practitionerName') }}
    +
    {{ licenseeName }}
    +
    +
    +
    {{ $t('common.state') }}
    +
    {{ stateContent }}
    +
    -
    -
    {{ $t('licensing.licenseType') }}
    -
    {{ licenseTypeAbbrev }}
    +
    +
    +
    {{ $t('licensing.licenseNumber') }}
    +
    {{ licenseNumber }}
    +
    +
    +
    {{ $t('licensing.licenseType') }}
    +
    {{ licenseTypeAbbrev }}
    +
    @@ -355,17 +359,35 @@ @keyup.esc="closeAddInvestigationModal" > + + +
    diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.less b/webroot/src/components/PrivilegeCard/PrivilegeCard.less index 220d5c4af..f6d044065 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.less +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.less @@ -201,6 +201,7 @@ @media @desktopWidth { flex-direction: row; + margin-bottom: 0.2rem; } } } diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index 1a1fda61c..6fb37eff2 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -567,25 +567,54 @@ class PrivilegeCard extends mixins(MixinForm) { privilegeTypeAbbrev } = this; - await this.$store.dispatch(`users/encumberPrivilegeRequest`, { - compact: compactType, - licenseeId, - privilegeState: stateAbbrev, - licenseType: privilegeTypeAbbrev.toLowerCase(), - encumbranceType: this.formData.encumberModalDisciplineAction.value, - ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) - ? { - npdbCategories: this.formData.encumberModalNpdbCategories.value, - } - : { - npdbCategory: this.formData.encumberModalNpdbCategory.value, - } - ), - startDate: this.formData.encumberModalStartDate.value, - }).catch((err) => { - this.modalErrorMessage = err?.message || this.$t('common.error'); - this.isFormError = true; - }); + if (this.selectedInvestigation) { + // Submit the encumbrance as part of a selected investigation update + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + investigationId, + encumbrance: { + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } else { + // Submit the encumbrance on its own + await this.$store.dispatch(`users/encumberPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } if (!this.isFormError) { this.isFormSuccessful = true; @@ -962,22 +991,17 @@ class PrivilegeCard extends mixins(MixinForm) { privilegeTypeAbbrev, } = this; const investigationId = this.selectedInvestigation?.id; - const errorMessages: Array = []; - await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + await this.$store.dispatch(`users/updateInvestigationPrivilegeRequest`, { compact: compactType, licenseeId, privilegeState: stateAbbrev, licenseType: privilegeTypeAbbrev.toLowerCase(), investigationId, }).catch((err) => { - errorMessages.push(err?.message || this.$t('common.error')); - }); - - if (errorMessages.length) { - this.modalErrorMessage = errorMessages.join('; '); + this.modalErrorMessage = err?.message || this.$t('common.error'); this.isFormError = true; - } + }); if (!this.isFormError) { this.isFormSuccessful = true; diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue index 3194c9271..ed909cbfc 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue @@ -419,17 +419,35 @@ @keyup.esc="closeAddInvestigationModal" > diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.less b/webroot/src/components/PrivilegeCard/PrivilegeCard.less index f6d044065..7cf308a69 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.less +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.less @@ -300,6 +300,12 @@ } } + .add-investigation-success-form { + display: flex; + flex-direction: column; + align-items: center; + } + .static-input { display: flex; flex-direction: column; diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index 4a6feb060..5aa21959d 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -804,10 +804,15 @@ class PrivilegeCard extends mixins(MixinForm) { } focusTrapAddInvestigationModal(event: KeyboardEvent): void { - const firstTabIndex = document.getElementById('add-investigation-modal-cancel-button'); - const lastTabIndex = (this.isFormValid && !this.isFormLoading && !this.isAddInvestigationModalSuccess) - ? document.getElementById(this.formData.addInvestigationModalContinue.id) + const { isAddInvestigationModalSuccess } = this; + const firstTabIndex = (isAddInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') : document.getElementById('add-investigation-modal-cancel-button'); + let lastTabIndex = document.getElementById('submit-modal-continue'); + + if (!this.isAddInvestigationModalSuccess && (!this.isFormValid || this.isFormLoading)) { + lastTabIndex = document.getElementById('add-investigation-modal-cancel-button'); + } if (event.shiftKey) { // shift + tab to last input diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue index 5b3da30a4..773283ce6 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue @@ -483,18 +483,19 @@ aria-live="polite" role="status" > -
    -

    {{ $t('licensing.confirmPrivilegeInvestigationStartSuccess') }}

    -
    -
    {{ licenseeName }}
    -
    {{ privilegeId }}
    -
    - +
    +
    +

    {{ $t('licensing.confirmPrivilegeInvestigationStartSuccess') }}

    +
    +
    {{ licenseeName }}
    +
    {{ privilegeId }}
    +
    + +
    diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index db1813615..0761bd20c 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -714,9 +714,9 @@ "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Add encumbrance", "confirmPrivilegeInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", "underInvestigationStatus": "Investigation", - "underInvestigationAlert1": "This practitioner is under investigation in {locations}.", - "underInvestigationAlert1MultipleLocations": "multiple states", - "underInvestigationAlert2": "Privileges can still be used while under investigation.", + "underInvestigationAlertLocation": "This practitioner is under investigation in {locations}.", + "underInvestigationAlertMultipleLocations": "multiple states", + "underInvestigationAlertStatus": "Privileges can still be used while under investigation.", "expiringIn": "Expiring in", "events": "Events", "expirationTimeExplanation": "Privilege dates use the UTC-4 time zone. This means that privileges will expire at 11:59PM US Eastern Time during Daylight Savings (summer), and at 10:59 PM in the US Eastern time zone during Standard Time (winter).", diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index 141aab6e7..5c8fdb75d 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -698,9 +698,9 @@ "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Agregar gravamen", "confirmPrivilegeInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", "underInvestigationStatus": "Investigación", - "underInvestigationAlert1": "Este practicante está bajo investigación en {locations}.", - "underInvestigationAlert1MultipleLocations": "múltiples estados", - "underInvestigationAlert2": "Los privilegios aún se pueden utilizar mientras se está bajo investigación.", + "underInvestigationAlertLocation": "Este practicante está bajo investigación en {locations}.", + "underInvestigationAlertMultipleLocations": "múltiples estados", + "underInvestigationAlertStatus": "Los privilegios aún se pueden utilizar mientras se está bajo investigación.", "expiringIn": "Expirando en", "events": "Eventos", "expirationTimeExplanation": "Las fechas privilegiadas utilizan la zona horaria UTC-4. Esto significa que los privilegios expirarán a las 11:59 p. m. hora del este de EE. UU. durante el horario de verano (verano) y a las 10:59 p. m. en la zona horaria del este de EE. UU. durante la hora estándar (invierno)", diff --git a/webroot/src/models/Licensee/Licensee.model.spec.ts b/webroot/src/models/Licensee/Licensee.model.spec.ts index 7bfd8ffde..71b5c8f93 100644 --- a/webroot/src/models/Licensee/Licensee.model.spec.ts +++ b/webroot/src/models/Licensee/Licensee.model.spec.ts @@ -1129,8 +1129,12 @@ describe('Licensee model', () => { expect(licensee.isEncumbered()).to.equal(true); }); it('should create a Licensee with under-investigation licenses and privileges', () => { + const homeState = new State({ abbrev: 'co' }); const underInvestigationLicense = new License({ - licenseNumber: 'encumbered-license', + issueState: homeState, + licenseNumber: 'investigation-license', + status: LicenseStatus.ACTIVE, + eligibility: EligibilityStatus.ELIGIBLE, investigations: [new Investigation({ state: new State({ abbrev: 'al' }), startDate: moment().subtract(1, 'day').format(serverDateFormat), @@ -1138,7 +1142,7 @@ describe('Licensee model', () => { })], }); const underInvestigationPrivilege = new License({ - licenseNumber: 'encumbered-privilege', + licenseNumber: 'investigation-privilege', investigations: [ new Investigation({ state: new State({ abbrev: 'al' }), @@ -1153,6 +1157,7 @@ describe('Licensee model', () => { ], }); const licensee = new Licensee({ + homeJurisdiction: homeState, licenses: [underInvestigationLicense], privileges: [underInvestigationPrivilege], }); @@ -1165,6 +1170,7 @@ describe('Licensee model', () => { new State({ abbrev: 'al' }), new State({ abbrev: 'co' }), ]); + expect(licensee.canPurchasePrivileges()).to.equal(true); }); it(`should handle 'unknown' currentHomeJurisdiction by falling back to licenseJurisdiction`, () => { const data = { diff --git a/webroot/src/models/Licensee/Licensee.model.ts b/webroot/src/models/Licensee/Licensee.model.ts index 56b5b56ab..a15355592 100644 --- a/webroot/src/models/Licensee/Licensee.model.ts +++ b/webroot/src/models/Licensee/Licensee.model.ts @@ -392,8 +392,7 @@ export class Licensee implements InterfaceLicensee { return !!this.purchaseEligibleLicenses().length && !this.isMilitaryStatusInitializing() && !this.isEncumbered() - && !this.hasEncumbranceLiftedWithinWaitPeriod() - && !this.isUnderInvestigation(); + && !this.hasEncumbranceLiftedWithinWaitPeriod(); } } diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.ts b/webroot/src/pages/LicensingDetail/LicensingDetail.ts index 483a2bedd..c486f7a40 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.ts +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.ts @@ -116,12 +116,12 @@ export default class LicensingDetail extends Vue { const investigationStates = this.licensee?.underInvestigationStates() || []; const statesContent = (investigationStates.length === 1) ? investigationStates[0].name() - : this.$t('licensing.underInvestigationAlert1MultipleLocations'); + : this.$t('licensing.underInvestigationAlertMultipleLocations'); let alertContent = ''; if (investigationStates.length) { - alertContent += `${this.$t('licensing.underInvestigationAlert1', { locations: statesContent })} - ${this.$t('licensing.underInvestigationAlert2')}`; + alertContent += `${this.$t('licensing.underInvestigationAlertLocation', { locations: statesContent })} + ${this.$t('licensing.underInvestigationAlertStatus')}`; } return alertContent;