diff --git a/webroot/src/app.config.ts b/webroot/src/app.config.ts index df803d58e..6b58f89c5 100644 --- a/webroot/src/app.config.ts +++ b/webroot/src/app.config.ts @@ -289,6 +289,7 @@ export const compacts = { // = Feature gate IDs = // ============================= export enum FeatureGates { + ENCUMBER_MULTI_CATEGORY = 'encumbrance-multi-category-flag', EXAMPLE_FEATURE_1 = 'test-feature-1', // Keep this ID in place for examples & tests } diff --git a/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.less b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.less new file mode 100644 index 000000000..963aa443e --- /dev/null +++ b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.less @@ -0,0 +1,63 @@ +// +// InputSelectMultiple.less +// CompactConnect +// +// Created by InspiringApps on 10/2/2025. +// + +.input-container { + .multi-select-description { + display: none; + font-size: @fontSizeSmaller; + font-style: italic; + + @media (hover: hover) { + display: flex; + } + } + + .select-dropdown { + resize: vertical; + } + + .selected-container { + display: flex; + flex-wrap: wrap; + width: 100%; + margin-top: 2rem; + + .selected-value { + @removeSize: 3.2rem; + + display: flex; + flex-grow: 0; + flex-shrink: 1; + align-items: center; + width: fit-content; + min-height: @removeSize; + margin-bottom: 1rem; + padding: 0 @removeSize 0 0.8rem; + border: 1px solid @fontColor; + border-radius: @borderRadiusPillShape; + color: @white; + font-size: @fontSize; + background-color: @fontColor; + + &:not(:last-child) { + margin-right: 1rem; + } + + .remove { + position: absolute; + top: 50%; + right: 0; + width: @removeSize; + height: @removeSize; + margin-left: auto; + transform: translateY(-50%); + cursor: pointer; + stroke: @white; + } + } + } +} diff --git a/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.spec.ts b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.spec.ts new file mode 100644 index 000000000..2ff2fda5b --- /dev/null +++ b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.spec.ts @@ -0,0 +1,19 @@ +// +// InputSelectMultiple.spec.ts +// CompactConnect +// +// Created by InspiringApps on 10/2/2025. +// + +import { expect } from 'chai'; +import { mountShallow } from '@tests/helpers/setup'; +import InputSelectMultiple from '@components/Forms/InputSelectMultiple/InputSelectMultiple.vue'; + +describe('InputSelectMultiple component', async () => { + it('should mount the component', async () => { + const wrapper = await mountShallow(InputSelectMultiple); + + expect(wrapper.exists()).to.equal(true); + expect(wrapper.findComponent(InputSelectMultiple).exists()).to.equal(true); + }); +}); diff --git a/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.ts b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.ts new file mode 100644 index 000000000..1ee3d9690 --- /dev/null +++ b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.ts @@ -0,0 +1,52 @@ +// +// InputSelectMultiple.ts +// CompactConnect +// +// Created by InspiringApps on 10/2/2025. +// + +import { + Component, + mixins, + toNative +} from 'vue-facing-decorator'; +import { ComputedRef } from 'vue'; +import MixinInput from '@components/Forms/_mixins/input.mixin'; +import CloseXIcon from '@components/Icons/CloseX/CloseX.vue'; + +interface SelectOption { + value: string | number; + name: string | ComputedRef; +} + +@Component({ + name: 'InputSelectMultiple', + components: { + CloseXIcon, + }, +}) +class InputSelectMultiple extends mixins(MixinInput) { + // + // Methods + // + getValueDisplay(value = ''): string | ComputedRef { + const selectedOption: SelectOption = this.formInput?.valueOptions?.find((option: SelectOption) => + option.value === value) || { value, name: '' }; + + return selectedOption?.name || ''; + } + + removeSelectedValue(value): void { + const { formInput } = this; + + if (Array.isArray(formInput.value)) { + (formInput.value as Array) = formInput.value.filter((selected) => selected !== value); + } + + formInput.validate(); + } +} + +export default toNative(InputSelectMultiple); + +// export default InputSelectMultiple; diff --git a/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.vue b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.vue new file mode 100644 index 000000000..b9f314040 --- /dev/null +++ b/webroot/src/components/Forms/InputSelectMultiple/InputSelectMultiple.vue @@ -0,0 +1,84 @@ + + + + + + diff --git a/webroot/src/components/LicenseCard/LicenseCard.ts b/webroot/src/components/LicenseCard/LicenseCard.ts index ef66ec115..e1355de10 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -16,10 +16,11 @@ import { ComputedRef, nextTick } from 'vue'; -import { dateFormatPatterns } from '@/app.config'; +import { dateFormatPatterns, FeatureGates } from '@/app.config'; import MixinForm from '@components/Forms/_mixins/form.mixin'; import InputDate from '@components/Forms/InputDate/InputDate.vue'; import InputSelect from '@components/Forms/InputSelect/InputSelect.vue'; +import InputSelectMultiple from '@components/Forms/InputSelectMultiple/InputSelectMultiple.vue'; import InputCheckbox from '@components/Forms/InputCheckbox/InputCheckbox.vue'; import InputButton from '@components/Forms/InputButton/InputButton.vue'; import InputSubmit from '@components/Forms/InputSubmit/InputSubmit.vue'; @@ -46,6 +47,7 @@ import moment from 'moment'; MockPopulate, InputDate, InputSelect, + InputSelectMultiple, InputCheckbox, InputButton, InputSubmit, @@ -89,6 +91,10 @@ class LicenseCard extends mixins(MixinForm) { // // Computed // + get featureGates(): typeof FeatureGates { + return FeatureGates; + } + get userStore() { return this.$store.state.user; } @@ -236,10 +242,12 @@ class LicenseCard extends mixins(MixinForm) { name: npdbType.name, })); - options.unshift({ - value: '', - name: computed(() => this.$t('common.selectOption')), - }); + if (!this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY)) { + options.unshift({ + value: '', + name: computed(() => this.$t('common.selectOption')), + }); + } return options; } @@ -272,13 +280,27 @@ class LicenseCard extends mixins(MixinForm) { validation: Joi.string().required().messages(this.joiMessages.string), valueOptions: this.encumberDisciplineOptions, }), - encumberModalNpdbCategory: new FormInput({ - id: 'npdb-category', - name: 'npdb-category', - label: computed(() => this.$t('licensing.npdbCategoryLabel')), - validation: Joi.string().required().messages(this.joiMessages.string), - valueOptions: this.npdbCategoryOptions, - }), + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + encumberModalNpdbCategories: new FormInput({ + id: 'npdb-categories', + name: 'npdb-categories', + label: computed(() => this.$t('licensing.npdbCategoryLabel')), + validation: Joi.array().min(1).messages(this.joiMessages.array), + valueOptions: this.npdbCategoryOptions, + value: [], + }), + } + : { + encumberModalNpdbCategory: new FormInput({ + id: 'npdb-category', + name: 'npdb-category', + label: computed(() => this.$t('licensing.npdbCategoryLabel')), + validation: Joi.string().required().messages(this.joiMessages.string), + valueOptions: this.npdbCategoryOptions, + }), + } + ), encumberModalStartDate: new FormInput({ id: 'encumber-start', name: 'encumber-start', @@ -310,7 +332,7 @@ class LicenseCard extends mixins(MixinForm) { const adverseActionInput = new FormInput({ id: `adverse-action-data-${adverseActionId}`, name: `adverse-action-data-${adverseActionId}`, - label: adverseAction.npdbTypeName(), + label: adverseAction.encumbranceTypeName(), isDisabled: Boolean(adverseAction.endDate), }); @@ -419,7 +441,14 @@ class LicenseCard extends mixins(MixinForm) { licenseState: stateAbbrev, licenseType: licenseTypeAbbrev.toLowerCase(), encumbranceType: this.formData.encumberModalDisciplineAction.value, - npdbCategory: this.formData.encumberModalNpdbCategory.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'); @@ -619,7 +648,11 @@ class LicenseCard extends mixins(MixinForm) { this.validateAll({ asTouched: true }); } else if (this.isEncumberLicenseModalDisplayed) { this.formData.encumberModalDisciplineAction.value = this.encumberDisciplineOptions[1]?.value; - this.formData.encumberModalNpdbCategory.value = this.npdbCategoryOptions[1]?.value; + if (this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY)) { + this.formData.encumberModalNpdbCategories.value = [this.npdbCategoryOptions[1]?.value]; + } else { + this.formData.encumberModalNpdbCategory.value = this.npdbCategoryOptions[1]?.value; + } this.formData.encumberModalStartDate.value = moment().format('YYYY-MM-DD'); await nextTick(); this.validateAll({ asTouched: true }); diff --git a/webroot/src/components/LicenseCard/LicenseCard.vue b/webroot/src/components/LicenseCard/LicenseCard.vue index 241924bb5..8341eb36b 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.vue +++ b/webroot/src/components/LicenseCard/LicenseCard.vue @@ -130,7 +130,10 @@
-
+
+ +
+
diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index ad41a20a1..b29d1b99e 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -16,11 +16,12 @@ import { ComputedRef, nextTick } from 'vue'; -import { dateFormatPatterns } from '@/app.config'; +import { dateFormatPatterns, FeatureGates } from '@/app.config'; import MixinForm from '@components/Forms/_mixins/form.mixin'; import InputTextarea from '@components/Forms/InputTextarea/InputTextarea.vue'; import InputDate from '@components/Forms/InputDate/InputDate.vue'; import InputSelect from '@components/Forms/InputSelect/InputSelect.vue'; +import InputSelectMultiple from '@components/Forms/InputSelectMultiple/InputSelectMultiple.vue'; import InputCheckbox from '@components/Forms/InputCheckbox/InputCheckbox.vue'; import InputButton from '@components/Forms/InputButton/InputButton.vue'; import InputSubmit from '@components/Forms/InputSubmit/InputSubmit.vue'; @@ -45,6 +46,7 @@ import moment from 'moment'; InputTextarea, InputDate, InputSelect, + InputSelectMultiple, InputCheckbox, InputButton, InputSubmit, @@ -73,6 +75,10 @@ class PrivilegeCard extends mixins(MixinForm) { // // Computed // + get featureGates(): typeof FeatureGates { + return FeatureGates; + } + get userStore() { return this.$store.state.user; } @@ -208,10 +214,12 @@ class PrivilegeCard extends mixins(MixinForm) { name: npdbType.name, })); - options.unshift({ - value: '', - name: computed(() => this.$t('common.selectOption')), - }); + if (!this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY)) { + options.unshift({ + value: '', + name: computed(() => this.$t('common.selectOption')), + }); + } return options; } @@ -264,13 +272,27 @@ class PrivilegeCard extends mixins(MixinForm) { validation: Joi.string().required().messages(this.joiMessages.string), valueOptions: this.encumberDisciplineOptions, }), - encumberModalNpdbCategory: new FormInput({ - id: 'npdb-category', - name: 'npdb-category', - label: computed(() => this.$t('licensing.npdbCategoryLabel')), - validation: Joi.string().required().messages(this.joiMessages.string), - valueOptions: this.npdbCategoryOptions, - }), + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + encumberModalNpdbCategories: new FormInput({ + id: 'npdb-categories', + name: 'npdb-categories', + label: computed(() => this.$t('licensing.npdbCategoryLabel')), + validation: Joi.array().min(1).messages(this.joiMessages.array), + valueOptions: this.npdbCategoryOptions, + value: [], + }), + } + : { + encumberModalNpdbCategory: new FormInput({ + id: 'npdb-category', + name: 'npdb-category', + label: computed(() => this.$t('licensing.npdbCategoryLabel')), + validation: Joi.string().required().messages(this.joiMessages.string), + valueOptions: this.npdbCategoryOptions, + }), + } + ), encumberModalStartDate: new FormInput({ id: 'encumber-start', name: 'encumber-start', @@ -302,7 +324,7 @@ class PrivilegeCard extends mixins(MixinForm) { const adverseActionInput = new FormInput({ id: `adverse-action-data-${adverseActionId}`, name: `adverse-action-data-${adverseActionId}`, - label: adverseAction.npdbTypeName(), + label: adverseAction.encumbranceTypeName(), isDisabled: Boolean(adverseAction.endDate), }); @@ -474,7 +496,14 @@ class PrivilegeCard extends mixins(MixinForm) { privilegeState: stateAbbrev, licenseType: privilegeTypeAbbrev.toLowerCase(), encumbranceType: this.formData.encumberModalDisciplineAction.value, - npdbCategory: this.formData.encumberModalNpdbCategory.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'); @@ -674,7 +703,11 @@ class PrivilegeCard extends mixins(MixinForm) { this.validateAll({ asTouched: true }); } else if (this.isEncumberPrivilegeModalDisplayed) { this.formData.encumberModalDisciplineAction.value = this.encumberDisciplineOptions[1]?.value; - this.formData.encumberModalNpdbCategory.value = this.npdbCategoryOptions[1]?.value; + if (this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY)) { + this.formData.encumberModalNpdbCategories.value = [this.npdbCategoryOptions[1]?.value]; + } else { + this.formData.encumberModalNpdbCategory.value = this.npdbCategoryOptions[1]?.value; + } this.formData.encumberModalStartDate.value = moment().format('YYYY-MM-DD'); await nextTick(); this.validateAll({ asTouched: true }); diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue index 55b0f0d81..582a9dba5 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue @@ -190,7 +190,10 @@
-
+
+ +
+
diff --git a/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.ts b/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.ts index fb1b1cbb5..9278bf336 100644 --- a/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.ts +++ b/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.ts @@ -6,12 +6,13 @@ // import { Component, mixins, toNative } from 'vue-facing-decorator'; -import { reactive, computed } from 'vue'; +import { reactive, computed, ComputedRef } from 'vue'; import MixinForm from '@components/Forms/_mixins/form.mixin'; import Section from '@components/Section/Section.vue'; import InputText from '@components/Forms/InputText/InputText.vue'; import InputTextarea from '@components/Forms/InputTextarea/InputTextarea.vue'; import InputSelect from '@components/Forms/InputSelect/InputSelect.vue'; +import InputSelectMultiple from '@components/Forms/InputSelectMultiple/InputSelectMultiple.vue'; import InputCheckbox from '@components/Forms/InputCheckbox/InputCheckbox.vue'; import InputRadioGroup from '@components/Forms/InputRadioGroup/InputRadioGroup.vue'; import InputDate from '@components/Forms/InputDate/InputDate.vue'; @@ -34,6 +35,7 @@ const joiPassword = Joi.extend(joiPasswordExtendCore); InputText, InputTextarea, InputSelect, + InputSelectMultiple, InputCheckbox, InputRadioGroup, InputDate, @@ -74,6 +76,15 @@ class ExampleForm extends mixins(MixinForm) { return this.$tm('common.states'); } + get npdbCategoryOptions(): Array<{ value: string, name: string | ComputedRef }> { + const options = this.$tm('licensing.npdbTypes').map((npdbType) => ({ + value: npdbType.key, + name: npdbType.name, + })); + + return options; + } + get statusOptions(): any { return this.$tm('styleGuide.statusOptions'); } @@ -113,7 +124,7 @@ class ExampleForm extends mixins(MixinForm) { showMax: true, enforceMax: true, }), - state: new FormInput({ + state: new FormInput({ // Single select id: 'state', name: 'state', label: computed(() => this.$t('common.stateJurisdiction')), @@ -122,6 +133,14 @@ class ExampleForm extends mixins(MixinForm) { valueOptions: [{ value: '', name: computed(() => this.$t('common.chooseOne')) }] .concat(this.states.map((state) => ({ value: state.abbrev, name: state.full }))), }), + states: new FormInput({ // Multi select + id: 'states', + name: 'states', + label: computed(() => this.$t('common.statesMultiple')), + validation: Joi.array().min(1).messages(this.joiMessages.array), + valueOptions: this.states.map((state) => ({ value: state.abbrev, name: state.full })), + value: [], + }), isSubscribed: new FormInput({ id: 'subscribe', name: 'subscribe', diff --git a/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.vue b/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.vue index fdb29192e..98f12156f 100644 --- a/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.vue +++ b/webroot/src/components/StyleGuide/ExampleForm/ExampleForm.vue @@ -18,6 +18,7 @@ :shouldResizeY="true" /> + diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index 8d2c0d842..59ada162f 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -18,6 +18,7 @@ "chooseOne": "Choose one", "select": "Select", "selectOption": "- Select -", + "selectMultipleKeys": "Hold Ctrl / Cmd key to select multiple", "selectFile": "Select file", "selectFiles": "Select files", "replaceFile": "Replace file", @@ -68,6 +69,7 @@ "address": "Address", "city": "City", "state": "State", + "statesMultiple": "States", "stateJurisdiction": "State / Jurisdiction", "zipCode": "Zip Code", "language": "Language", diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index 8c7d409c2..35aad9141 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -18,6 +18,7 @@ "chooseOne": "Elige uno", "select": "Seleccionar", "selectOption": "- Seleccionar -", + "selectMultipleKeys": "Mantenga presionada la tecla Ctrl / Cmd para seleccionar varios", "selectFile": "Seleccione Archivo", "selectFiles": "Selecciona archivos", "replaceFile": "Reemplazar el archivo", @@ -69,6 +70,7 @@ "formValidationErrorMessage": "Corrija los errores en el formulario que se muestra en rojo y marque la casilla que reconoce que no hay reembolsos.", "city": "Ciudad", "state": "Estado", + "statesMultiple": "Estados", "stateJurisdiction": "Estado o Jurisdicción", "zipCode": "Código postal", "language": "Idioma", diff --git a/webroot/src/models/AdverseAction/AdverseAction.model.spec.ts b/webroot/src/models/AdverseAction/AdverseAction.model.spec.ts index 6d6b6288c..f87d70ab4 100644 --- a/webroot/src/models/AdverseAction/AdverseAction.model.spec.ts +++ b/webroot/src/models/AdverseAction/AdverseAction.model.spec.ts @@ -47,6 +47,7 @@ describe('AdverseAction model', () => { expect(adverseAction.state).to.be.an.instanceof(State); expect(adverseAction.type).to.equal(null); expect(adverseAction.npdbType).to.equal(null); + expect(adverseAction.npdbTypes).to.matchPattern([]); expect(adverseAction.creationDate).to.equal(null); expect(adverseAction.startDate).to.equal(null); expect(adverseAction.endDate).to.equal(null); @@ -58,6 +59,7 @@ describe('AdverseAction model', () => { expect(adverseAction.hasEndDate()).to.equal(false); expect(adverseAction.encumbranceTypeName()).to.equal(''); expect(adverseAction.npdbTypeName()).to.equal(''); + expect(adverseAction.getNpdbTypeName()).to.equal(''); expect(adverseAction.isActive()).to.equal(false); }); it('should create an AdverseAction model with specific values', () => { @@ -69,6 +71,7 @@ describe('AdverseAction model', () => { type: 'test-type', encumbranceType: 'test-encumbranceType', npdbType: 'test-npdbType', + npdbTypes: ['test-npdbType'], creationDate: 'test-creationDate', startDate: 'test-startDate', endDate: 'test-endDate', @@ -83,6 +86,7 @@ describe('AdverseAction model', () => { expect(adverseAction.state).to.be.an.instanceof(State); expect(adverseAction.type).to.equal(data.type); expect(adverseAction.npdbType).to.equal(data.npdbType); + expect(adverseAction.npdbTypes).to.matchPattern(data.npdbTypes); expect(adverseAction.creationDate).to.equal(data.creationDate); expect(adverseAction.startDate).to.equal(data.startDate); expect(adverseAction.endDate).to.equal(data.endDate); @@ -94,6 +98,7 @@ describe('AdverseAction model', () => { expect(adverseAction.hasEndDate()).to.equal(true); expect(adverseAction.encumbranceTypeName()).to.equal(''); expect(adverseAction.npdbTypeName()).to.equal(''); + expect(adverseAction.getNpdbTypeName('Other')).to.equal('Other'); expect(adverseAction.isActive()).to.equal(false); }); it('should create an AdverseAction model with specific values (startDate but no endDate)', () => { @@ -148,6 +153,7 @@ describe('AdverseAction model', () => { type: 'test-type', encumbranceType: 'fine', clinicalPrivilegeActionCategory: 'Non-compliance With Requirements', + clinicalPrivilegeActionCategories: ['Non-compliance With Requirements'], creationDate: moment.utc().format(serverDatetimeFormat), effectiveStartDate: moment().subtract(1, 'day').format(serverDateFormat), effectiveLiftDate: moment().add(1, 'day').format(serverDateFormat), @@ -163,6 +169,7 @@ describe('AdverseAction model', () => { expect(adverseAction.state.name()).to.equal('Alabama'); expect(adverseAction.type).to.equal(data.type); expect(adverseAction.npdbType).to.equal(data.clinicalPrivilegeActionCategory); + expect(adverseAction.npdbTypes).to.matchPattern(data.clinicalPrivilegeActionCategories); expect(adverseAction.creationDate).to.equal(data.creationDate); expect(adverseAction.startDate).to.equal(data.effectiveStartDate); expect(adverseAction.endDate).to.equal(data.effectiveLiftDate); @@ -178,6 +185,17 @@ describe('AdverseAction model', () => { expect(adverseAction.hasEndDate()).to.equal(true); expect(adverseAction.encumbranceTypeName()).to.equal('Fine'); expect(adverseAction.npdbTypeName()).to.equal('Non-compliance With Requirements'); + expect(adverseAction.getNpdbTypeName(data.clinicalPrivilegeActionCategories[0])).to.equal('Non-compliance With Requirements'); expect(adverseAction.isActive()).to.equal(true); }); + it('should create an AdverseAction model with specific values through serializer (invalid data type from server)', () => { + const data = { + clinicalPrivilegeActionCategories: 'Non-compliance With Requirements', + }; + const adverseAction = AdverseActionSerializer.fromServer(data); + + // Test field values + expect(adverseAction).to.be.an.instanceof(AdverseAction); + expect(adverseAction.npdbTypes).to.matchPattern([]); + }); }); diff --git a/webroot/src/models/AdverseAction/AdverseAction.model.ts b/webroot/src/models/AdverseAction/AdverseAction.model.ts index 58ffa48a5..7a93b3b3f 100644 --- a/webroot/src/models/AdverseAction/AdverseAction.model.ts +++ b/webroot/src/models/AdverseAction/AdverseAction.model.ts @@ -23,6 +23,7 @@ export interface InterfaceAdverseActionCreate { type?: string | null; encumbranceType?: string | null; npdbType?: string | null; + npdbTypes?: Array; creationDate?: string | null; startDate?: string | null; endDate?: string | null; @@ -40,6 +41,7 @@ export class AdverseAction implements InterfaceAdverseActionCreate { public type? = null; public encumbranceType? = null; public npdbType? = null; + public npdbTypes? = []; public creationDate? = null; public startDate? = null; public endDate? = null; @@ -89,6 +91,14 @@ export class AdverseAction implements InterfaceAdverseActionCreate { return typeName; } + public getNpdbTypeName(npdbType: string): string { + const npdbTypes = this.$tm('licensing.npdbTypes') || []; + const npdbTypeRecord = npdbTypes.find((translate) => translate.key === npdbType); + const typeName = npdbTypeRecord?.name || ''; + + return typeName; + } + public isActive(): boolean { // Determine whether the adverse action is currently in effect const { startDate, endDate } = this; @@ -124,6 +134,9 @@ export class AdverseActionSerializer { type: json.type, encumbranceType: json.encumbranceType, npdbType: json.clinicalPrivilegeActionCategory, + npdbTypes: Array.isArray(json.clinicalPrivilegeActionCategories) + ? json.clinicalPrivilegeActionCategories + : [], creationDate: json.creationDate, startDate: json.effectiveStartDate, endDate: json.effectiveLiftDate, diff --git a/webroot/src/network/data.api.ts b/webroot/src/network/data.api.ts index c15d7f167..e8d1d0525 100644 --- a/webroot/src/network/data.api.ts +++ b/webroot/src/network/data.api.ts @@ -165,10 +165,20 @@ export class DataApi { * @param {string} licenseType The license type. * @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 encumberLicense(compact, licenseeId, licenseState, licenseType, encumbranceType, npdbCategory, startDate) { + public encumberLicense( + compact, + licenseeId, + licenseState, + licenseType, + encumbranceType, + npdbCategory, + npdbCategories, + startDate + ) { return licenseDataApi.encumberLicense( compact, licenseeId, @@ -176,6 +186,7 @@ export class DataApi { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ); } @@ -222,6 +233,7 @@ export class DataApi { * @param {string} licenseType The license type. * @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. */ @@ -232,6 +244,7 @@ export class DataApi { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ) { return licenseDataApi.encumberPrivilege( @@ -241,6 +254,7 @@ export class DataApi { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ); } diff --git a/webroot/src/network/licenseApi/data.api.ts b/webroot/src/network/licenseApi/data.api.ts index 5185d3251..6dfc3b2bd 100644 --- a/webroot/src/network/licenseApi/data.api.ts +++ b/webroot/src/network/licenseApi/data.api.ts @@ -5,7 +5,7 @@ // Created by InspiringApps on 6/18/24. // -import { authStorage, tokens } from '@/app.config'; +import { authStorage, tokens, FeatureGates } from '@/app.config'; import axios, { AxiosInstance } from 'axios'; import { requestError, @@ -260,6 +260,7 @@ export class LicenseDataApi implements DataApiInterface { * @param {string} licenseType The license type. * @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. */ @@ -270,11 +271,20 @@ export class LicenseDataApi implements DataApiInterface { licenseType: string, encumbranceType: string, npdbCategory: string, + npdbCategories: Array, startDate: string ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/licenses/jurisdiction/${licenseState}/licenseType/${licenseType}/encumbrance`, { encumbranceType, - clinicalPrivilegeActionCategory: npdbCategory, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: npdbCategories, + } + : { + clinicalPrivilegeActionCategory: npdbCategory, + } + ), encumbranceEffectiveDate: startDate, }); @@ -337,6 +347,7 @@ export class LicenseDataApi implements DataApiInterface { * @param {string} licenseType The license type. * @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. */ @@ -347,11 +358,20 @@ export class LicenseDataApi implements DataApiInterface { licenseType: string, encumbranceType: string, npdbCategory: string, + npdbCategories: Array, startDate: string ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${privilegeState}/licenseType/${licenseType}/encumbrance`, { encumbranceType, - clinicalPrivilegeActionCategory: npdbCategory, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: npdbCategories, + } + : { + clinicalPrivilegeActionCategory: npdbCategory, + } + ), encumbranceEffectiveDate: startDate, }); diff --git a/webroot/src/network/mocks/mock.data.api.ts b/webroot/src/network/mocks/mock.data.api.ts index d27c3dc47..02e7b4bbc 100644 --- a/webroot/src/network/mocks/mock.data.api.ts +++ b/webroot/src/network/mocks/mock.data.api.ts @@ -224,7 +224,18 @@ export class DataApi { } // Encumber License for a licensee. - public encumberLicense(compact, licenseeId, licenseState, licenseType, encumbranceType, npdbCategory, startDate) { + public encumberLicense( + compact, + licenseeId, + licenseState, + licenseType, + encumbranceType, + npdbCategory, + npdbCategories, + startDate + ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + if (!compact) { return Promise.reject(new Error('failed license encumber')); } @@ -236,7 +247,14 @@ export class DataApi { licenseState, licenseType, encumbranceType, - npdbCategory, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories, + } + : { + npdbCategory, + } + ), startDate, })); } @@ -281,8 +299,11 @@ export class DataApi { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + if (!compact) { return Promise.reject(new Error('failed privilege encumber')); } @@ -294,7 +315,14 @@ export class DataApi { privilegeState, licenseType, encumbranceType, - npdbCategory, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories, + } + : { + npdbCategory, + } + ), startDate, })); } diff --git a/webroot/src/store/users/users.actions.ts b/webroot/src/store/users/users.actions.ts index 7d9367eb2..3c3940db3 100644 --- a/webroot/src/store/users/users.actions.ts +++ b/webroot/src/store/users/users.actions.ts @@ -132,6 +132,7 @@ export default { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate }: any) => { commit(MutationTypes.ENCUMBER_LICENSE_REQUEST); @@ -142,6 +143,7 @@ export default { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ).then(async (response) => { dispatch('encumberLicenseSuccess'); @@ -228,6 +230,7 @@ export default { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate }: any) => { commit(MutationTypes.ENCUMBER_PRIVILEGE_REQUEST); @@ -238,6 +241,7 @@ export default { licenseType, encumbranceType, npdbCategory, + npdbCategories, startDate ).then(async (response) => { dispatch('encumberPrivilegeSuccess'); diff --git a/webroot/vue.config.js b/webroot/vue.config.js index 8c4a00d26..50e765b2f 100644 --- a/webroot/vue.config.js +++ b/webroot/vue.config.js @@ -90,6 +90,22 @@ const htmlPlugin = (args) => { return args; }; +/** + * extract-css-plugin configuration (https://github.com/webpack/mini-css-extract-plugin) + * Included with Vue CLI, so we are just chaining + * @param {array} args The webpack plugin options array. + * @return {array} The webpack plugin options array (updated by reference). + */ +const extractCssPlugin = (args) => { + const opts = args[0]; + + // Suppress CSS order warnings from mini-css-extract-plugin + // These warnings do not affect functionality and are common in apps that use code splitting / chunking + opts.ignoreOrder = true; + + return args; +}; + /** * fork-ts-checker-webpack-plugin (https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) * Included with Vue CLI, so we are just chaining @@ -277,6 +293,11 @@ module.exports = { // Update the Typescript-Checker plugin settings config.plugin('fork-ts-checker').tap(forkTsCheckerWebpackPlugin); + // Update the CSS-Build plugin settings (only exists for builds) + if (env === ENV_PRODUCTION) { + config.plugin('extract-css').tap(extractCssPlugin); + } + // Inject common LESS styles into each module // https://cli.vuejs.org/guide/css.html#automatic-imports const types = ['vue-modules', 'vue', 'normal-modules', 'normal']; diff --git a/webroot/yarn.lock b/webroot/yarn.lock index 33422e13f..f8e1cf757 100644 --- a/webroot/yarn.lock +++ b/webroot/yarn.lock @@ -12076,9 +12076,9 @@ tmp@^0.0.33: os-tmpdir "~1.0.2" tmp@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== to-fast-properties@^2.0.0: version "2.0.0"