diff --git a/.github/workflows/check-webroot.yml b/.github/workflows/check-webroot.yml index a8180b8e5..452a3586d 100644 --- a/.github/workflows/check-webroot.yml +++ b/.github/workflows/check-webroot.yml @@ -33,6 +33,7 @@ jobs: VUE_APP_ROBOTS_META: noindex,nofollow VUE_APP_API_STATE_ROOT: https://api.test.jcc.iaapi.io VUE_APP_API_LICENSE_ROOT: https://api.test.jcc.iaapi.io + VUE_APP_API_SEARCH_ROOT: https://search.test.jcc.iaapi.io VUE_APP_COGNITO_REGION: us-east-1 VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: https://ia-cc-staff-test.auth.us-east-1.amazoncognito.com VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ secrets.DEV_WEBROOT_COGNITO_CLIENT_ID_STAFF }} @@ -88,6 +89,7 @@ jobs: VUE_APP_ROBOTS_META: ${{ env.VUE_APP_ROBOTS_META }} VUE_APP_API_STATE_ROOT: ${{ env.VUE_APP_API_STATE_ROOT }} VUE_APP_API_LICENSE_ROOT: ${{ env.VUE_APP_API_LICENSE_ROOT }} + VUE_APP_API_SEARCH_ROOT: ${{ env.VUE_APP_API_SEARCH_ROOT }} VUE_APP_COGNITO_REGION: ${{ env.VUE_APP_COGNITO_REGION }} VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_STAFF }} VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ env.VUE_APP_COGNITO_CLIENT_ID_STAFF }} diff --git a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py index d1a70b5f9..7af4d997f 100644 --- a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py +++ b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py @@ -62,6 +62,7 @@ def __init__( 'VUE_APP_ROBOTS_META': robots_meta, 'VUE_APP_API_STATE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', 'VUE_APP_API_LICENSE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', + 'VUE_APP_API_SEARCH_ROOT': f'{HTTPS_PREFIX}search.{persistent_stack_app_config_values.api_domain_name}', 'VUE_APP_API_USER_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', 'VUE_APP_COGNITO_REGION': 'us-east-1', 'VUE_APP_COGNITO_AUTH_DOMAIN_STAFF': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.staff_cognito_domain}', diff --git a/webroot/.env.example b/webroot/.env.example index 185652395..856b3de93 100644 --- a/webroot/.env.example +++ b/webroot/.env.example @@ -5,6 +5,7 @@ VUE_APP_DOMAIN=http://localhost:3018 VUE_APP_ROBOTS_META=noindex,nofollow VUE_APP_API_STATE_ROOT=https://api.test.jcc.iaapi.io VUE_APP_API_LICENSE_ROOT=https://api.test.jcc.iaapi.io +VUE_APP_API_SEARCH_ROOT=https://search.test.jcc.iaapi.io VUE_APP_API_USER_ROOT=https://api.test.jcc.iaapi.io VUE_APP_COGNITO_REGION=us-east-1 VUE_APP_COGNITO_AUTH_DOMAIN_STAFF=https://staff-auth.test.jcc.iaapi.io @@ -13,6 +14,7 @@ VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE=https://licensee-auth.test.jcc.iaapi.io VUE_APP_COGNITO_CLIENT_ID_LICENSEE=topd4vhftng5cfm3ccgkb6ejd VUE_APP_RECAPTCHA_KEY=6Le-3bgqAAAAAILDVUKkRnAF9SSzb8o9uv5lY7Ih VUE_APP_STATSIG_KEY=TODO +VUE_APP_STATSIG_DISABLED=false VUE_APP_MOCK_API=false VUE_APP_MOCK_API_PAYMENT_LOGIN_ID=TODO VUE_APP_MOCK_API_PAYMENT_CLIENT_KEY=TODO diff --git a/webroot/README.md b/webroot/README.md index 03ab04ddd..956a03799 100644 --- a/webroot/README.md +++ b/webroot/README.md @@ -76,6 +76,14 @@ - Prod: `https://api.compactconnect.org` - _Local_ :arrow_heading_down: - `https://api.test.jcc.iaapi.io` + - **`VUE_APP_API_SEARCH_ROOT`** + - _Server_ :arrow_heading_up: + - IA Test: `https://search.test.jcc.iaapi.io` + - CSG Test: `https://search.test.compactconnect.org` + - Beta: `https://search.beta.compactconnect.org` + - Prod: `https://search.compactconnect.org` + - _Local_ :arrow_heading_down: + - `https://search.test.jcc.iaapi.io` - **`VUE_APP_COGNITO_REGION`** - _Server_ :arrow_heading_up: - IA Test: `us-east-1` @@ -132,6 +140,14 @@ - Prod: TODO - _Local_ :arrow_heading_down: - TODO + - **`VUE_APP_STATSIG_DISABLED`** + - _Server_ :arrow_heading_up: + - IA Test: `false` + - CSG Test: `false` + - Beta: `false` + - Prod: `false` + - _Local_ :arrow_heading_down: + - `true` or `false` as needed - **`VUE_APP_MOCK_API`** :arrow_heading_down: - Only used for local development - `true` if mock API should be used diff --git a/webroot/src/components/Forms/InputSubmit/InputSubmit.ts b/webroot/src/components/Forms/InputSubmit/InputSubmit.ts index fe88549fb..dadf4a8e1 100644 --- a/webroot/src/components/Forms/InputSubmit/InputSubmit.ts +++ b/webroot/src/components/Forms/InputSubmit/InputSubmit.ts @@ -20,6 +20,8 @@ class InputSubmit extends mixins(MixinInput) { @Prop({ default: '' }) private label?: string; @Prop({ default: true }) private isEnabled?: boolean; @Prop({ default: false }) private isWarning?: boolean; + @Prop({ default: false }) private isTransparent?: boolean; + @Prop({ default: false }) private isTextLike?: boolean; } export default toNative(InputSubmit); diff --git a/webroot/src/components/Forms/InputSubmit/InputSubmit.vue b/webroot/src/components/Forms/InputSubmit/InputSubmit.vue index e7836f822..c535fe488 100644 --- a/webroot/src/components/Forms/InputSubmit/InputSubmit.vue +++ b/webroot/src/components/Forms/InputSubmit/InputSubmit.vue @@ -36,6 +36,8 @@ class="input-submit" :class="{ 'warning': isWarning, + 'transparent': isTransparent, + 'text-like': isTextLike, }" :aria-describedby="(formInput.successMessage) ? `${formInput.id}-success` : `${formInput.id}-error`" :aria-errormessage="`${formInput.id}-error`" diff --git a/webroot/src/components/Licensee/LicenseeList/LicenseeList.less b/webroot/src/components/Licensee/LicenseeList/LicenseeList.less index 5ddce8005..149f16d15 100644 --- a/webroot/src/components/Licensee/LicenseeList/LicenseeList.less +++ b/webroot/src/components/Licensee/LicenseeList/LicenseeList.less @@ -2,7 +2,7 @@ // LicenseeList.less // CompactConnect // -// Created by InspiringApps on 7/1/2024. +// Created by InspiringApps on 12/1/2025. // .licensee-list-container { @@ -36,10 +36,19 @@ @media @desktopWidth { order: 1; width: auto; + max-width: 75%; margin-top: 0; margin-right: 2.4rem; } + @media @largeDesktopWidth { + max-width: 85%; + } + + @media @extraLargeDesktopWidth { + max-width: 95%; + } + .title { padding: 0 0.4rem; } @@ -50,6 +59,7 @@ .search-terms-reset { width: 2rem; + min-width: 2rem; margin-left: auto; padding: 0.4rem 0.2rem; cursor: pointer; diff --git a/webroot/src/components/Licensee/LicenseeList/LicenseeList.spec.ts b/webroot/src/components/Licensee/LicenseeList/LicenseeList.spec.ts index c3a468ff2..c7a67eff7 100644 --- a/webroot/src/components/Licensee/LicenseeList/LicenseeList.spec.ts +++ b/webroot/src/components/Licensee/LicenseeList/LicenseeList.spec.ts @@ -2,25 +2,12 @@ // LicenseeList.spec.ts // CompactConnect // -// Created by InspiringApps on 7/1/2024. +// Created by InspiringApps on 12/1/2025. // -import chaiMatchPattern from 'chai-match-pattern'; -import chai from 'chai'; -import { mountShallow, mountFull } from '@tests/helpers/setup'; +import { expect } from 'chai'; +import { mountShallow } from '@tests/helpers/setup'; import LicenseeList from '@components/Licensee/LicenseeList/LicenseeList.vue'; -import { Compact, CompactType } from '@models/Compact/Compact.model'; -import sinon from 'sinon'; - -chai.use(chaiMatchPattern); - -const { expect } = chai; -const lastKey = 'lastKey'; -const prevLastKey = 'prevLastKey'; -const populateComponentStorePagingKeys = (component) => { - component.$store.dispatch('license/setStoreLicenseeLastKey', lastKey); - component.$store.dispatch('license/setStoreLicenseePrevLastKey', prevLastKey); -}; describe('LicenseeList component', async () => { it('should mount the component', async () => { @@ -29,113 +16,4 @@ describe('LicenseeList component', async () => { expect(wrapper.exists()).to.equal(true); expect(wrapper.findComponent(LicenseeList).exists()).to.equal(true); }); - it('should successfully re-fetch data with previous paging key if going back a page', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const fetchListData = sinon.spy(); - - component.fetchListData = fetchListData; - component.isInitialFetchCompleted = true; - populateComponentStorePagingKeys(component); - - await component.paginationChange({ firstIndex: 0, prevNext: -1 }); - - expect(component.prevKey).to.equal(prevLastKey); - expect(component.nextKey).to.equal(''); - expect(fetchListData.calledOnce).to.equal(true); - }); - it('should successfully re-fetch data with next paging key if going forward a page', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const fetchListData = sinon.spy(); - - component.fetchListData = fetchListData; - component.isInitialFetchCompleted = true; - populateComponentStorePagingKeys(component); - - await component.paginationChange({ firstIndex: 0, prevNext: 1 }); - - expect(component.prevKey).to.equal(''); - expect(component.nextKey).to.equal(lastKey); - expect(fetchListData.calledOnce).to.equal(true); - }); - it('should successfully re-fetch data when returning to first page', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const fetchListData = sinon.spy(); - - component.fetchListData = fetchListData; - component.isInitialFetchCompleted = true; - populateComponentStorePagingKeys(component); - - await component.paginationChange({ firstIndex: 0, prevNext: undefined }); - - expect(component.prevKey).to.equal(''); - expect(component.nextKey).to.equal(''); - expect(fetchListData.calledOnce).to.equal(true); - }); - it('should successfully not re-fetch data if page change before initial fetch completes', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const fetchListData = sinon.spy(); - - component.fetchListData = fetchListData; - component.isInitialFetchCompleted = false; - populateComponentStorePagingKeys(component); - - await component.paginationChange({ firstIndex: 0, prevNext: 1 }); - - expect(component.prevKey).to.equal(''); - expect(component.nextKey).to.equal(lastKey); - expect(fetchListData.notCalled).to.equal(true); - }); - it('should successfully not re-fetch data if page change from search results', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const fetchListData = sinon.spy(); - - component.fetchListData = fetchListData; - component.isInitialFetchCompleted = true; - populateComponentStorePagingKeys(component); - - await component.paginationChange({ firstIndex: 0, prevNext: 0 }); - - expect(component.prevKey).to.equal(''); - expect(component.nextKey).to.equal(''); - expect(fetchListData.notCalled).to.equal(true); - }); - it('should successfully fetch data with expected search params (no params)', async () => { - const wrapper = await mountFull(LicenseeList); - const component = wrapper.vm; - const requestConfig = await component.fetchListData(); - - expect(requestConfig).to.matchPattern({ - jurisdiction: undefined, - licenseeFirstName: undefined, - licenseeLastName: undefined, - '...': '', - }); - }); - it('should successfully fetch data with expected search params (all params)', async () => { - const wrapper = await mountShallow(LicenseeList); - const component = wrapper.vm; - const testParams = { - firstName: 'firstName', - lastName: 'lastName', - state: 'state', - }; - - await component.$store.dispatch('user/setCurrentCompact', new Compact({ type: CompactType.ASLP })); - await component.$store.dispatch('license/setStoreSearch', testParams); - - const requestConfig = await component.fetchListData(); - - expect(requestConfig).to.matchPattern({ - compact: CompactType.ASLP, - jurisdiction: testParams.state, - licenseeFirstName: testParams.firstName, - licenseeLastName: testParams.lastName, - '...': '', - }); - }); }); diff --git a/webroot/src/components/Licensee/LicenseeList/LicenseeList.ts b/webroot/src/components/Licensee/LicenseeList/LicenseeList.ts index 835bca7e4..4fc69afc7 100644 --- a/webroot/src/components/Licensee/LicenseeList/LicenseeList.ts +++ b/webroot/src/components/Licensee/LicenseeList/LicenseeList.ts @@ -2,7 +2,7 @@ // LicenseeList.ts // CompactConnect // -// Created by InspiringApps on 7/1/2024. +// Created by InspiringApps on 12/1/2025. // import { @@ -11,15 +11,16 @@ import { Prop, toNative } from 'vue-facing-decorator'; +import { serverDateFormat, displayDateFormat } from '@/app.config'; import ListContainer from '@components/Lists/ListContainer/ListContainer.vue'; -import LicenseeSearch, { LicenseSearch } from '@components/Licensee/LicenseeSearch/LicenseeSearch.vue'; +import LicenseeSearch, { LicenseSearch, SearchTypes } from '@components/Licensee/LicenseeSearch/LicenseeSearch.vue'; import LicenseeRow from '@components/Licensee/LicenseeRow/LicenseeRow.vue'; import CloseX from '@components/Icons/CloseX/CloseX.vue'; import { SortDirection } from '@store/sorting/sorting.state'; -import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, PageChangeConfig } from '@store/pagination/pagination.state'; -import { PageExhaustError } from '@store/pagination'; -import { RequestParamsInterfaceLocal } from '@network/licenseApi/data.api'; +import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from '@store/pagination/pagination.state'; +import { SearchParamsInterfaceLocal } from '@network/searchApi/data.api'; import { State } from '@models/State/State.model'; +import moment from 'moment'; @Component({ name: 'LicenseeList', @@ -39,9 +40,8 @@ class LicenseeList extends Vue { // hasSearched = false; shouldShowSearchModal = false; + searchErrorOverride = ''; isInitialFetchCompleted = false; - prevKey = ''; - nextKey = ''; // // Lifecycle @@ -97,68 +97,117 @@ class LicenseeList extends Vue { return (this.isPublicSearch) ? this.userStore.currentCompact?.abbrev() || '' : ''; } - get searchDisplayFirstName(): string { - const delimiter = (this.searchDisplayCompact) ? ', ' : ''; - let displayFirstName = ''; + get searchDisplayFullName(): string { + const { firstName = '', lastName = '' } = this.searchParams; - if (this.searchParams.firstName) { - displayFirstName = `${delimiter}${this.searchParams.firstName}` || ''; + return `${firstName} ${lastName}`.trim(); + } + + get searchDisplayHomeState(): string { + const { homeState } = this.searchParams; + + return (homeState) ? `${this.$t('licensing.homeState')}: ${new State({ abbrev: homeState }).name()}` : ''; + } + + get searchDisplayPrivilegeState(): string { + const { privilegeState } = this.searchParams; + + return (privilegeState) ? `${this.$t('licensing.privilegeState')}: ${new State({ abbrev: privilegeState }).name()}` : ''; + } + + get searchDisplayPrivilegePurchaseDates(): string { + const { privilegePurchaseStartDate = '', privilegePurchaseEndDate = '' } = this.searchParams; + let displayDates = ''; + + if (privilegePurchaseStartDate || privilegePurchaseEndDate) { + const startDate = (privilegePurchaseStartDate) + ? moment(privilegePurchaseStartDate, serverDateFormat).format(displayDateFormat) + : '∞'; + const endDate = (privilegePurchaseEndDate) + ? moment(privilegePurchaseEndDate, serverDateFormat).format(displayDateFormat) + : '∞'; + + displayDates = `${this.$t('licensing.purchaseDate')}: ${startDate}-${endDate}`; } - return displayFirstName; + return displayDates; } - get searchDisplayLastName(): string { - const delimiter = (this.searchDisplayCompact && !this.searchDisplayFirstName) ? ', ' : ''; - const subDelimiter = (this.searchDisplayFirstName) ? ' ' : ''; - let displayLastName = ''; + get searchDisplayMilitaryStatus(): string { + const { militaryStatus } = this.searchParams; + let displayStatus = ''; - if (this.searchParams.lastName) { - displayLastName = `${delimiter}${subDelimiter}${this.searchParams.lastName}` || ''; + if (militaryStatus) { + const statusOptions = this.$tm('military.militaryStatusOptions') || []; + const selectedOption = statusOptions.find((statusOption) => statusOption.key === militaryStatus); + + if (selectedOption?.name) { + displayStatus = `${this.$t('military.militaryStatusTitle')}: ${selectedOption.name}`; + } + } + + return displayStatus; + } + + get searchDisplayInvestigationStatus(): string { + const { investigationStatus } = this.searchParams; + let displayStatus = ''; + + if (investigationStatus) { + const statusOptions = this.$tm('licensing.investigationStatusOptions') || []; + const selectedOption = statusOptions.find((statusOption) => statusOption.key === investigationStatus); + + if (selectedOption?.name) { + displayStatus = `${selectedOption.name}`; + } } - return displayLastName; + return displayStatus; } - get searchDisplayState(): string { - const { state } = this.searchParams; - const { searchDisplayCompact, searchDisplayFirstName, searchDisplayLastName } = this; - const delimiter = (searchDisplayCompact || searchDisplayFirstName || searchDisplayLastName) ? ', ' : ''; - let displayState = ''; + get searchDisplayEncumberDates(): string { + const { encumberStartDate = '', encumberEndDate = '' } = this.searchParams; + let displayDates = ''; - if (state) { - const stateModel = new State({ abbrev: state }); + if (encumberStartDate || encumberEndDate) { + const startDate = (encumberStartDate) + ? moment(encumberStartDate, serverDateFormat).format(displayDateFormat) + : '∞'; + const endDate = (encumberEndDate) + ? moment(encumberEndDate, serverDateFormat).format(displayDateFormat) + : '∞'; - displayState = `${delimiter}${stateModel.name()}`; + displayDates = `${this.$t('licensing.encumbered')}: ${startDate}-${endDate}`; } - return displayState; + return displayDates; + } + + get searchDisplayNpi(): string { + const { npi = '' } = this.searchParams; + + return (npi) ? `${this.$t('licensing.npi')}: ${npi}`.trim() : ''; } get searchDisplayAll(): string { - const { - searchDisplayCompact, - searchDisplayFirstName, - searchDisplayLastName, - searchDisplayState - } = this; - - return [ - searchDisplayCompact, - searchDisplayFirstName, - searchDisplayLastName, - searchDisplayState - ].join('').trim(); + const joined = [ + this.searchDisplayCompact, + this.searchDisplayFullName, + this.searchDisplayHomeState, + this.searchDisplayPrivilegeState, + this.searchDisplayPrivilegePurchaseDates, + this.searchDisplayMilitaryStatus, + this.searchDisplayInvestigationStatus, + this.searchDisplayEncumberDates, + this.searchDisplayNpi + ].join(', ').trim(); + + return joined.replace(/(^[,\s]+)|([,\s]+$)/g, '').replace(/(,\s)\1+/g, ', '); // Replace repeated commas with single comma } get sortOptions(): Array { const options = [ - // Temp for limited server sorting support - // { value: 'firstName', name: this.$t('common.firstName') }, { value: 'lastName', name: this.$t('common.lastName'), isDefault: true }, - // { value: 'licenseStates', name: this.$t('licensing.homeState') }, - // { value: 'privilegeStates', name: this.$t('licensing.privileges') }, - // { value: 'status', name: this.$t('licensing.status') }, ]; return options; @@ -168,7 +217,6 @@ class LicenseeList extends Vue { const record = { firstName: this.$t('common.firstName'), lastName: this.$t('common.lastName'), - ssnMaskedPartial: () => this.$t('licensing.ssn'), homeJurisdictionDisplay: () => this.$t('licensing.homeState'), privilegeStatesDisplay: () => this.$t('licensing.privileges'), statusDisplay: () => this.$t('licensing.status'), @@ -190,12 +238,15 @@ class LicenseeList extends Vue { paginationId: this.listId, newPage: 1, }); + this.searchErrorOverride = ''; this.fetchListData(); - if (!this.hasSearched) { - this.hasSearched = true; - } else { - this.toggleSearch(); + if (!params.isDirectExport) { + if (!this.hasSearched) { + this.hasSearched = true; + } else { + this.toggleSearch(); + } } } @@ -232,7 +283,6 @@ class LicenseeList extends Vue { async setDefaultPaging(shouldForce = false) { const { listId } = this; const { page, size } = this.paginationStore.paginationMap[this.listId] || {}; - const { prevLastKey } = this.licenseStore; if (!page || shouldForce) { await this.$store.dispatch('pagination/updatePaginationPage', { @@ -247,48 +297,37 @@ class LicenseeList extends Vue { newSize: DEFAULT_PAGE_SIZE, }); } + } + + async fetchListData(): Promise { + const { searchParams } = this; + const { searchType } = searchParams; + const requestConfig = this.prepareSearchBody(); - if (prevLastKey) { - this.prevKey = prevLastKey; + if (searchType === SearchTypes.PROVIDER) { + // Provider licensee search is a standard REST JSON call + await this.$store.dispatch('license/getLicenseesSearchRequest', { + params: { ...requestConfig } + }); + } else if (searchType === SearchTypes.PRIVILEGE) { + // Privilege search is a file download call + requestConfig.isForPrivileges = true; + await this.handlePrivilegeDownload(requestConfig); } + + this.isInitialFetchCompleted = true; + + return requestConfig; } - async fetchListData() { + prepareSearchBody(): SearchParamsInterfaceLocal { const { searchParams } = this; const sorting = this.sortingStore.sortingMap[this.listId]; const { option, direction } = sorting || {}; const pagination = this.paginationStore.paginationMap[this.listId]; const { page, size } = pagination || {}; - const requestConfig: RequestParamsInterfaceLocal = {}; - - // Sorting params - if (option) { - const serverSortByMap = { - firstName: 'givenName', - lastName: 'familyName', - lastUpdate: 'dateOfUpdate', - }; - - requestConfig.sortBy = serverSortByMap[option]; - } - - if (direction) { - const serverSortDirectionMap = { - asc: 'ascending', - desc: 'descending', - }; - - requestConfig.sortDirection = serverSortDirectionMap[direction]; - } - - // Paging params - if (page && !this.licenseStore.error) { - if (this.nextKey && page !== 1) { - requestConfig.getNextPage = true; - } else if (this.prevKey) { - requestConfig.getPrevPage = true; - } - } + const requestConfig: SearchParamsInterfaceLocal = {}; + const { isDirectExport } = searchParams; // Search params requestConfig.isPublic = this.isPublicSearch; @@ -305,73 +344,92 @@ class LicenseeList extends Vue { if (searchParams?.lastName) { requestConfig.licenseeLastName = searchParams.lastName; } - if (searchParams?.state) { - requestConfig.jurisdiction = searchParams.state.toLowerCase(); + if (searchParams?.homeState) { + requestConfig.homeState = searchParams.homeState.toLowerCase(); + } + if (searchParams?.privilegeState) { + requestConfig.privilegeState = searchParams.privilegeState.toLowerCase(); + } + if (searchParams?.privilegePurchaseStartDate) { + requestConfig.privilegePurchaseStartDate = searchParams.privilegePurchaseStartDate; + } + if (searchParams?.privilegePurchaseEndDate) { + requestConfig.privilegePurchaseEndDate = searchParams.privilegePurchaseEndDate; + } + if (searchParams?.militaryStatus) { + requestConfig.militaryStatus = searchParams.militaryStatus; + } + if (searchParams?.investigationStatus) { + requestConfig.investigationStatus = searchParams.investigationStatus; + } + if (searchParams?.encumberStartDate) { + requestConfig.encumberStartDate = searchParams.encumberStartDate; + } + if (searchParams?.encumberEndDate) { + requestConfig.encumberEndDate = searchParams.encumberEndDate; + } + if (searchParams?.npi) { + requestConfig.npi = searchParams.npi; } - // Make fetch request - await this.$store.dispatch('license/getLicenseesRequest', { - params: { - ...requestConfig, - pageNum: page, - pageSize: size, - } - }); + // Paging params + if (!isDirectExport) { + requestConfig.pageNumber = page; + requestConfig.pageSize = size; + } - this.isInitialFetchCompleted = true; + // Sorting params + if (!isDirectExport) { + if (option) { + const serverSortByMap = { + lastName: 'familyName', + }; - // If we've reached the end of paging - if (this.licenseStore.error instanceof PageExhaustError && page > 1) { - // Support for limited server paging support: - // The server does not respond with how many total records there are, only keys to fetch - // the current or next page. So the frontend can't know it's the end of paging until we get back 0 records. - // At that point, we no longer have usable prevLastKey & lastKey values from the server, and need to re-fetch - // the last page to get stable. - - // Update pagination store page - this.$store.dispatch('pagination/updatePaginationPage', { - paginationId: this.listId, - newPage: page - 1, - }); - // Re-fetch with prevLastKey - await this.$store.dispatch('license/getLicenseesRequest', { - params: { - ...requestConfig, - getPrevPage: true, - getNextPage: false, - pageNum: page, - pageSize: size, - } - }); - // After fetch, delete lastKey from the store (to disable "next" button) - this.$store.dispatch('license/setStoreLicenseeLastKey', null); + requestConfig.sortBy = serverSortByMap[option]; + } + + if (direction) { + requestConfig.sortDirection = direction; + } } return requestConfig; } + async handlePrivilegeDownload(requestConfig: SearchParamsInterfaceLocal): Promise { + let errorMessage = ''; + const response = await this.$store.dispatch('license/getPrivilegesRequest', { + params: { ...requestConfig } + }).catch((error) => { + errorMessage = error?.message || error; + }); + + if (errorMessage) { + this.searchErrorOverride = errorMessage; + } else if (response) { + const { fileUrl } = response; + const tempLink = document.createElement('a'); + + if (!fileUrl) { + this.searchErrorOverride = this.$t('serverErrors.searchErrorGeneral'); + } else { + tempLink.href = fileUrl; + tempLink.target = '_blank'; + tempLink.rel = 'noopener noreferrer'; + tempLink.download = `privilege_export.csv`; + tempLink.click(); + } + } + } + async sortingChange() { if (this.isInitialFetchCompleted) { await this.fetchListData(); } } - // Match pageChange() @Prop signature from /components/Lists/Pagination/Pagination.ts - async paginationChange({ firstIndex, prevNext }: PageChangeConfig) { - const isInitialInProgress = firstIndex === 0 && prevNext === 0; - - if (prevNext === -1) { - this.prevKey = this.licenseStore.prevLastKey; - this.nextKey = ''; - } else if (prevNext === 1) { - this.prevKey = ''; - this.nextKey = this.licenseStore.lastKey; - } else { - this.prevKey = ''; - this.nextKey = ''; - } - - if (!isInitialInProgress && this.isInitialFetchCompleted) { + async paginationChange() { + if (this.isInitialFetchCompleted) { await this.fetchListData(); } } diff --git a/webroot/src/components/Licensee/LicenseeList/LicenseeList.vue b/webroot/src/components/Licensee/LicenseeList/LicenseeList.vue index 0eeaffece..ad7627972 100644 --- a/webroot/src/components/Licensee/LicenseeList/LicenseeList.vue +++ b/webroot/src/components/Licensee/LicenseeList/LicenseeList.vue @@ -2,7 +2,7 @@ LicenseeList.vue CompactConnect - Created by InspiringApps on 7/1/2024. + Created by InspiringApps on 12/1/2025. -->