diff --git a/webroot/src/components/LicenseCard/LicenseCard.less b/webroot/src/components/LicenseCard/LicenseCard.less index 29c499cce..3e19bdecf 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.less +++ b/webroot/src/components/LicenseCard/LicenseCard.less @@ -125,7 +125,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; @@ -154,6 +154,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: ''; + } + } } } } @@ -244,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 { @@ -251,17 +277,23 @@ border-color: @fontColor; } - .encumber-license-form-input-container { + .encumber-license-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; @@ -272,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; @@ -291,11 +338,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -307,7 +349,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -323,13 +366,40 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .add-investigation-success-form { + display: flex; + flex-direction: column; + align-items: center; + } + + .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 e1355de10..08d979833 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -37,6 +37,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'; @@ -73,8 +74,15 @@ class LicenseCard extends mixins(MixinForm) { isEncumberLicenseModalSuccess = false; isUnencumberLicenseModalDisplayed = false; isUnencumberLicenseModalSuccess = false; + isAddInvestigationModalDisplayed = false; + isAddInvestigationModalSuccess = false; + isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; + isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -214,14 +222,30 @@ 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 { 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, @@ -252,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); } @@ -268,6 +308,10 @@ class LicenseCard extends mixins(MixinForm) { this.initFormInputsEncumberLicense(); } else if (this.isUnencumberLicenseModalDisplayed) { this.initFormInputsUnencumberLicense(); + } else if (this.isAddInvestigationModalDisplayed) { + this.initFormInputsAddInvestigation(); + } else if (this.isEndInvestigationModalDisplayed) { + this.initFormInputsEndInvestigation(); } } @@ -341,6 +385,38 @@ class LicenseCard 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.endDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -435,30 +511,60 @@ 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; await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); this.isEncumberLicenseModalSuccess = true; + this.selectedInvestigation = null; } this.endFormLoading(); @@ -624,6 +730,240 @@ class LicenseCard 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 { 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 + 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, + licenseTypeAbbrev + } = this; + + await this.$store.dispatch(`users/createInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.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.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(); @@ -637,6 +977,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); } @@ -669,6 +1013,15 @@ class LicenseCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((investigation) => !investigation.hasEndDate()) + .map(async (investigation) => { + this.clickEndInvestigationItem(investigation); + 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 8341eb36b..a0839c1d1 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.vue +++ b/webroot/src/components/LicenseCard/LicenseCard.vue @@ -54,6 +54,26 @@ > {{ $t('licensing.unencumber') }} +
  • + {{ $t('licensing.addInvestigation') }} +
  • +
  • + {{ $t('licensing.endInvestigation') }} +
  • @@ -111,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 }}
    +
    @@ -325,6 +349,246 @@
    + + + + + +
    diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.less b/webroot/src/components/PrivilegeCard/PrivilegeCard.less index 8db594b95..7cf308a69 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,16 @@ .form-row { margin-bottom: 1.6rem; + + &.static-container { + display: flex; + flex-direction: column; + + @media @desktopWidth { + flex-direction: row; + margin-bottom: 0.2rem; + } + } } #notes { @@ -185,17 +211,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 +238,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 +272,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -241,7 +283,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -257,13 +300,40 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .add-investigation-success-form { + display: flex; + flex-direction: column; + align-items: center; + } + + .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 b29d1b99e..5aa21959d 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,8 +69,15 @@ class PrivilegeCard extends mixins(MixinForm) { isEncumberPrivilegeModalSuccess = false; isUnencumberPrivilegeModalDisplayed = false; isUnencumberPrivilegeModalSuccess = false; + isAddInvestigationModalDisplayed = false; + isAddInvestigationModalSuccess = false; + isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; + isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -186,14 +194,30 @@ 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 { 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, @@ -224,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); } @@ -242,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(); } } @@ -333,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.endDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -454,6 +530,7 @@ class PrivilegeCard extends mixins(MixinForm) { event?.preventDefault(); this.isEncumberPrivilegeModalDisplayed = false; this.isEncumberPrivilegeModalSuccess = false; + this.selectedInvestigation = null; } focusTrapEncumberPrivilegeModal(event: KeyboardEvent): void { @@ -490,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; @@ -587,7 +693,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') @@ -615,7 +721,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') @@ -679,6 +785,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 { 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 + 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; + + await this.$store.dispatch(`users/updateInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.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(); @@ -692,6 +1032,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); } @@ -724,6 +1068,15 @@ class PrivilegeCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((investigation) => !investigation.hasEndDate()) + .map(async (investigation) => { + this.clickEndInvestigationItem(investigation); + 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..773283ce6 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,245 @@
    + + + + + +
    diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index cf8eac659..0761bd20c 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -687,6 +687,36 @@ "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.", + "confirmLicenseInvestigationEndSubmitWithoutEncumber": "No encumbrance", + "confirmLicenseInvestigationEndSubmitWithEncumber": "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.", + "confirmPrivilegeInvestigationEndSubmitWithoutEncumber": "No encumbrance", + "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Add encumbrance", + "confirmPrivilegeInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", + "underInvestigationStatus": "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 ddfcce1a4..5c8fdb75d 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -671,6 +671,36 @@ "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.", + "confirmLicenseInvestigationEndSubmitWithoutEncumber": "Sin gravamen", + "confirmLicenseInvestigationEndSubmitWithEncumber": "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.", + "confirmPrivilegeInvestigationEndSubmitWithoutEncumber": "Sin gravamen", + "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Agregar gravamen", + "confirmPrivilegeInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", + "underInvestigationStatus": "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/Investigation/Investigation.model.spec.ts b/webroot/src/models/Investigation/Investigation.model.spec.ts new file mode 100644 index 000000000..e5baf42dd --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.spec.ts @@ -0,0 +1,167 @@ +// +// 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); + expect(investigation.endDate).to.equal(null); + + // Test methods + expect(investigation.startDateDisplay()).to.equal(''); + expect(investigation.updateDateDisplay()).to.equal(''); + expect(investigation.endDateDisplay()).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-updateDate', + endDate: '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.updateDate).to.equal(data.updateDate); + 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.endDateDisplay()).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.endDate).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 = { + endDate: 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.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.isActive()).to.equal(true); + }); + it('should create an Investigation model with specific values (endDate of today should count as lifted)', () => { + const data = { + startDate: moment().format(serverDateFormat), + endDate: 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.endDate).to.equal(data.endDate); + + // 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().format(serverDateFormat), + endDate: 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); + expect(investigation.endDate).to.equal(data.endDate); + + // 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.endDateDisplay()).to.equal( + moment(data.endDate, 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..5c60b8a81 --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.ts @@ -0,0 +1,113 @@ +// +// 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; + endDate?: 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; + public endDate? = 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 endDateDisplay(): string { + return dateDisplay(this.endDate); + } + + public hasEndDate(): boolean { + return Boolean(this.endDate); + } + + public isActive(): boolean { + // Determine whether the investigation is currently in effect + const { startDate, 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, + endDate: json.endDate, + }; + + 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..71b5c8f93 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,50 @@ 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 homeState = new State({ abbrev: 'co' }); + const underInvestigationLicense = new 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), + updateDate: moment().add(1, 'day').format(serverDateFormat), + })], + }); + const underInvestigationPrivilege = new License({ + licenseNumber: 'investigation-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({ + homeJurisdiction: homeState, + 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' }), + ]); + expect(licensee.canPurchasePrivileges()).to.equal(true); + }); 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..a15355592 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,59 @@ 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 (investigation.isActive() + && 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 (investigation.isActive() + && 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)); diff --git a/webroot/src/network/data.api.ts b/webroot/src/network/data.api.ts index cdde3cb5a..0b1d640eb 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,56 @@ 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} privilegeState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType + ) { + return licenseDataApi.createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + 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} privilegeState 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, + privilegeState, + licenseType, + investigationId, + encumbrance + ) { + return licenseDataApi.updatePrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + 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..2f52256a3 100644 --- a/webroot/src/network/licenseApi/data.api.ts +++ b/webroot/src/network/licenseApi/data.api.ts @@ -316,6 +316,77 @@ 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 {string} investigationId The Investigation ID. + * @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}`, { + action: 'close', + ...(encumbrance + ? { + encumbrance: { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + }, + } + : {} + ), + }); + + return serverResponse; + } + /** * DELETE Privilege for a licensee. * @param {string} compact The compact string ID (aslp, octp, coun). @@ -403,6 +474,77 @@ 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} privilegeState The 2-character state abbreviation for the Privilege. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public async createPrivilegeInvestigation( + compact: string, + licenseeId: string, + privilegeState: string, + licenseType: string + ) { + const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${privilegeState}/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} privilegeState The 2-character state abbreviation for the Privilege. + * @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, + privilegeState: 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/${privilegeState}/licenseType/${licenseType}/investigation/${investigationId}`, { + action: 'close', + ...(encumbrance + ? { + encumbrance: { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + }, + } + : {} + ), + }); + + 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..5956658ec 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,50 @@ export class DataApi { })); } + // Create Privilege Investigation for a licensee. + public createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType + ) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation create')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + privilegeState, + licenseType, + })); + } + + // Update Privilege Investigation for a licensee. + public updatePrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance + ) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation update')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance, + })); + } + // Get full SSN for licensee public getLicenseeSsn(compact, licenseeId) { return wait(500).then(() => ({ diff --git a/webroot/src/network/mocks/mock.data.ts b/webroot/src/network/mocks/mock.data.ts index a3d44692a..2c37f3251 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: { @@ -761,6 +769,30 @@ export const licensees = { liftingUser: null, }, ], + 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: moment().subtract(1, 'week').format(serverDatetimeFormat), + endDate: 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(1, 'month').format(serverDatetimeFormat), + endDate: moment().subtract(2, 'weeks').format(serverDatetimeFormat), + }, + ], }, ], privilegeJurisdictions: [ @@ -911,6 +943,58 @@ 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, + endDate: 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(1, 'month').format(serverDatetimeFormat), + endDate: 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, + endDate: null, + }, + ], + }, ], }, { @@ -1836,6 +1920,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..c486f7a40 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.underInvestigationAlertMultipleLocations'); + let alertContent = ''; + + if (investigationStates.length) { + alertContent += `${this.$t('licensing.underInvestigationAlertLocation', { locations: statesContent })} + ${this.$t('licensing.underInvestigationAlertStatus')}`; + } + + 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 }} +