From b76de3be0783a66182c56720ba7b6a3099b6ea63 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 5 Feb 2024 15:14:23 -0800 Subject: [PATCH] 19331 & 19330 - Unlinked + Linked Short Name Mapping tables (#2700) * Adding in some code that could be potentially shared * Add in new linting rule * Minor changes to add in title * PascalCase * use v-sanitize * Minor update to populate table and hide pagination option * Wire this up to pay-api * rename fields * Filters working roughly * Remove debounce for now, not working currently, small UX tweaks * Small CSS fixes for top of tabs * Move table title over a bit * Fix filters being stuck on * Add in branchName to auth-api to update pay-api * Revert "Add in branchName to auth-api to update pay-api" This reverts commit 1d35c4d8c4f4a2b39e42ea9c703139c1bcb7efa0. * Styling, counts * Move component name * one more spot * Add in actions, remove redundant SCSS reuse old debounce that works * Move short names under different route * add in staff prot for now * Refactor, wire up infinite scroll * up page limit * Remove overrides, move some code * Remove comments * Rename, refactor into model * Minor change * Fix linting errors * minor changes * cleanup * cleanup * SCSS cleanup for view buttons * Test fixes * tidy up infinite scroll * Add in comments * Fix code smell * More typing fixes * Get rid of code duplication * Lint fixes * Add more return hints * Code complexity issue * Spacing issue * Unlinked shortname tab * Missing titles from refactor * Fix import linking * refactor SCSS * Spacing * refactor into scss * Typo * Use enum * More cleanup * Wire up counts * Use composable so we can share across the two components. * ShortNameLookup component * add in reachedEnd = false when setFiltering * remove debugger * refactor infinite scroll callback * refactor to reactive * Fix default value * Addin more typing * rename ShortName -> Shortname * use ShortName not Shortname * ShortNameLookup * Fix any * fix model reference * Short-name fix * Add comments * fix comment * Adding UnlinkedShortNameResults and State * Remove starting data * Add in unit test * Finish off unit test * Cleanup * Add in sandbox.restore(), restore formatAmount back to what it was, unit tests should pass for now * Remove duplicate code * Comment out ShortNameLookup * Code cleanup * Add in defineComponent so build doesn't fail * Cleanup * Testing * Remove onclick for now * refactor * Put back new Promise * Fix lint issue * Deposit date range support * Interface updates * Fix unit test * Display date range dates --------- Co-authored-by: Rodrigo Barraza --- auth-web/.eslintrc.js | 1 + auth-web/package-lock.json | 13 + auth-web/package.json | 1 + auth-web/src/assets/scss/ShortnameTables.scss | 18 + auth-web/src/assets/scss/actions.scss | 25 ++ auth-web/src/assets/scss/layout.scss | 4 + .../account-info/AccountDetails.vue | 2 +- .../transaction/TransactionsDataTable.vue | 16 +- .../src/components/auth/home/BcscPanel.vue | 2 +- .../manage-business/AffiliatedEntityTable.vue | 5 +- .../manage-business/AffiliationAction.vue | 31 -- .../components/datatable/BaseVDataTable.vue | 46 ++- .../datatable/components/HeaderFilter.vue | 8 +- .../components/pay/LinkedShortNameTable.vue | 184 ++++++++++ .../src/components/pay/ShortNameLookup.vue | 304 +++++++++++++++++ .../components/pay/UnlinkedShortNameTable.vue | 320 ++++++++++++++++++ .../composables/short-name-table-factory.ts | 88 +++++ auth-web/src/models/pay/short-name.ts | 71 ++++ auth-web/src/routes/router.ts | 8 + auth-web/src/services/payment.services.ts | 24 ++ auth-web/src/util/common-util.ts | 2 +- auth-web/src/util/constants.ts | 5 + auth-web/src/util/debounce.ts | 2 +- .../views/auth/staff/StaffDashboardView.vue | 12 +- .../src/views/pay/ShortNameMappingView.vue | 145 ++++++++ .../components/LinkedShortNameTable.spec.ts | 109 ++++++ .../components/UnlinkedShortNameTable.spec.ts | 120 +++++++ 27 files changed, 1505 insertions(+), 61 deletions(-) create mode 100644 auth-web/src/assets/scss/ShortnameTables.scss create mode 100644 auth-web/src/assets/scss/actions.scss create mode 100644 auth-web/src/components/pay/LinkedShortNameTable.vue create mode 100644 auth-web/src/components/pay/ShortNameLookup.vue create mode 100644 auth-web/src/components/pay/UnlinkedShortNameTable.vue create mode 100644 auth-web/src/composables/short-name-table-factory.ts create mode 100644 auth-web/src/models/pay/short-name.ts create mode 100644 auth-web/src/views/pay/ShortNameMappingView.vue create mode 100644 auth-web/tests/unit/components/LinkedShortNameTable.spec.ts create mode 100644 auth-web/tests/unit/components/UnlinkedShortNameTable.spec.ts diff --git a/auth-web/.eslintrc.js b/auth-web/.eslintrc.js index 0aab35cf5c..e9e266381a 100644 --- a/auth-web/.eslintrc.js +++ b/auth-web/.eslintrc.js @@ -48,6 +48,7 @@ module.exports = { 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true }], 'vue/multi-word-component-names': ['error', { 'ignores': ['Transactions'] }], + 'vue/component-name-in-template-casing': ['error', 'PascalCase'], // Not ideal but shallowOnly option isn't working for this, so leaving it off for now. // https://eslint.vuejs.org/rules/no-mutating-props.html 'vue/no-mutating-props': 'off' diff --git a/auth-web/package-lock.json b/auth-web/package-lock.json index ea84768dcc..130e6809b2 100644 --- a/auth-web/package-lock.json +++ b/auth-web/package-lock.json @@ -46,6 +46,7 @@ "devDependencies": { "@intlify/vue-i18n-loader": "^1.1.0", "@pinia/testing": "^0.1.3", + "@types/lodash": "^4.14.202", "@types/mime-types": "^2.1.0", "@types/vuelidate": "^0.7.13", "@typescript-eslint/eslint-plugin": "^6.4.0", @@ -1888,6 +1889,12 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/mime-types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", @@ -14918,6 +14925,12 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "@types/mime-types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", diff --git a/auth-web/package.json b/auth-web/package.json index d29f63b90d..c5307a89f1 100644 --- a/auth-web/package.json +++ b/auth-web/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@intlify/vue-i18n-loader": "^1.1.0", "@pinia/testing": "^0.1.3", + "@types/lodash": "^4.14.202", "@types/mime-types": "^2.1.0", "@types/vuelidate": "^0.7.13", "@typescript-eslint/eslint-plugin": "^6.4.0", diff --git a/auth-web/src/assets/scss/ShortnameTables.scss b/auth-web/src/assets/scss/ShortnameTables.scss new file mode 100644 index 0000000000..3425702f72 --- /dev/null +++ b/auth-web/src/assets/scss/ShortnameTables.scss @@ -0,0 +1,18 @@ +::v-deep { + + .base-table__header > tr:first-child > th { + padding: 0 0 0 0 !important; + } + .base-table__header__filter { + padding-left: 16px; + padding-right: 4px; + } + .base-table__item-row { + color: #495057; + font-weight: bold; + } + .base-table__item-cell { + padding: 16px 0 16px 16px; + vertical-align: middle; + } +} diff --git a/auth-web/src/assets/scss/actions.scss b/auth-web/src/assets/scss/actions.scss new file mode 100644 index 0000000000..de5add1e70 --- /dev/null +++ b/auth-web/src/assets/scss/actions.scss @@ -0,0 +1,25 @@ +// For the dropdown text color. +::v-deep .theme--light.v-list-item .v-list-item__action-text, .theme--light.v-list-item .v-list-item__subtitle { + color: $app-blue; + font-weight: normal; + .v-icon.v-icon { + color: $app-blue; + } +} + +.new-actions { + height:30px; + .open-action-btn { + font-size: .875rem; + box-shadow: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin-right: 1px; + } + + .more-actions-btn { + box-shadow: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} diff --git a/auth-web/src/assets/scss/layout.scss b/auth-web/src/assets/scss/layout.scss index f2b3e1879a..8f2cb6a70d 100644 --- a/auth-web/src/assets/scss/layout.scss +++ b/auth-web/src/assets/scss/layout.scss @@ -84,3 +84,7 @@ font-size: 16px; color: #495057 } + +.soft-corners-top { + border-radius: 5px 5px 0 0; +} diff --git a/auth-web/src/components/auth/account-settings/account-info/AccountDetails.vue b/auth-web/src/components/auth/account-settings/account-info/AccountDetails.vue index f047a40af9..6fbf4eff81 100644 --- a/auth-web/src/components/auth/account-settings/account-info/AccountDetails.vue +++ b/auth-web/src/components/auth/account-settings/account-info/AccountDetails.vue @@ -67,7 +67,7 @@
-
- ({{ transactions.totalResults }})
- - +
@@ -157,7 +157,7 @@ import { useTransactions } from '@/composables' export default defineComponent({ name: 'TransactionsDataTable', - components: { BaseVDataTable, DatePicker, IconTooltip }, + components: { BaseVDataTable: BaseVDataTable, DatePicker, IconTooltip }, props: { extended: { default: false }, headers: { default: [] as BaseTableHeaderI[] } diff --git a/auth-web/src/components/auth/home/BcscPanel.vue b/auth-web/src/components/auth/home/BcscPanel.vue index 54ffd4784a..7305db068d 100644 --- a/auth-web/src/components/auth/home/BcscPanel.vue +++ b/auth-web/src/components/auth/home/BcscPanel.vue @@ -86,7 +86,7 @@ Create a BC Registries Account - + diff --git a/auth-web/src/components/auth/manage-business/AffiliatedEntityTable.vue b/auth-web/src/components/auth/manage-business/AffiliatedEntityTable.vue index f47a87df7a..6cba89707d 100644 --- a/auth-web/src/components/auth/manage-business/AffiliatedEntityTable.vue +++ b/auth-web/src/components/auth/manage-business/AffiliatedEntityTable.vue @@ -27,7 +27,7 @@ - - + diff --git a/auth-web/src/components/auth/manage-business/AffiliationAction.vue b/auth-web/src/components/auth/manage-business/AffiliationAction.vue index 5462a18095..6a20727420 100644 --- a/auth-web/src/components/auth/manage-business/AffiliationAction.vue +++ b/auth-web/src/components/auth/manage-business/AffiliationAction.vue @@ -646,37 +646,6 @@ export default defineComponent({ text-align: center; } - .actions { - height:30px; - width: 140px; - - .open-action { - border-right: 1px solid $gray1; - } - - .open-action-btn { - font-size: .875rem; - box-shadow: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - margin-right: 1px; - max-height: 36px !important; - min-height: 36px !important; - } - - .more-actions-btn { - box-shadow: none; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - max-height: 36px !important; - min-height: 36px !important; - } - - .v-btn + .v-btn { - margin-left: 0.5rem; - } - } - .new-actions { height:30px; width: 240px; diff --git a/auth-web/src/components/datatable/BaseVDataTable.vue b/auth-web/src/components/datatable/BaseVDataTable.vue index 252171020e..bda5aaef9c 100644 --- a/auth-web/src/components/datatable/BaseVDataTable.vue +++ b/auth-web/src/components/datatable/BaseVDataTable.vue @@ -19,6 +19,21 @@ @@ -161,7 +176,10 @@ export default defineComponent({ filters: { default: { isActive: false, filterPayload: {} }, required: false }, customPagination: { default: false }, highlightIndex: { default: -1 }, - highlightClass: { type: String, default: '' } + highlightClass: { type: String, default: '' }, + title: { type: String, default: '' }, + useObserver: { type: Boolean, required: false }, + observerCallback: { type: Function as PropType<() => Promise>, required: false, default: null } }, emits: ['update-table-options'], setup (props, { emit }) { @@ -180,7 +198,10 @@ export default defineComponent({ const reachedEnd = ref(false) const getNext = _.debounce(async () => { - if (!props.loading && !reachedEnd.value && state.sortedItems.length > state.visibleItems.length) { + if (props.loading) return + if (props.observerCallback) { + reachedEnd.value = await props.observerCallback() + } else if (!reachedEnd.value && state.sortedItems.length > state.visibleItems.length) { currentPage.value++ const start = (currentPage.value - 1) * perPage.value const end = start + perPage.value @@ -201,17 +222,21 @@ export default defineComponent({ } watch(() => state.sortedItems, () => { - if (props.setItems && props.pageHide) { + if (props.setItems && !props.observerCallback) { state.visibleItems = state.sortedItems.slice(0, perPage.value) firstItem.value = state.visibleItems[0] currentPage.value = 1 reachedEnd.value = false scrollToTop() } + if (props.observerCallback) { + state.visibleItems = state.sortedItems + } }, { immediate: true }) const setFiltering = (filter: boolean) => { state.filtering = filter + reachedEnd.value = false } const setSortedItems = (items: object[]) => { @@ -254,6 +279,11 @@ export default defineComponent({ @import '@/assets/scss/theme.scss'; .base-table { + h2 { + font-size: 1.125rem; + letter-spacing: 0.25px; + } + &__header { &__filter { @@ -295,6 +325,10 @@ export default defineComponent({ flex-shrink: 0; } + .table-title-row { + background-color: $BCgovBlue0; + } + ::v-deep .v-data-footer { min-width: 100%; } diff --git a/auth-web/src/components/datatable/components/HeaderFilter.vue b/auth-web/src/components/datatable/components/HeaderFilter.vue index d17ab3e92f..0ce4840f8e 100644 --- a/auth-web/src/components/datatable/components/HeaderFilter.vue +++ b/auth-web/src/components/datatable/components/HeaderFilter.vue @@ -40,7 +40,7 @@ import { BaseSelectFilter, BaseTextFilter } from '../resources/base-filters' import { PropType, defineComponent, reactive } from '@vue/composition-api' import { BaseTableHeaderI } from '../interfaces' -import _ from 'lodash' +import debounce from '@/util/debounce' import { headerTypes } from '@/resources/table-headers/affiliations-table/headers' const tempHeader = { @@ -71,15 +71,15 @@ export default defineComponent({ sortedItems: [...props.sortedItems] }) as unknown) as BaseTableStateI - const serverSideFilter = async (header: BaseTableHeaderI) => { + const serverSideFilter = debounce(async (header: BaseTableHeaderI) => { props.setFiltering(true) await header.customFilter.filterApiFn(header.customFilter.value) props.setFiltering(false) - } + }) const filter = async (header: BaseTableHeaderI) => { if (header.customFilter.filterApiFn) { - _.debounce(serverSideFilter(header), 500) + await serverSideFilter(header) } else { applyFilters(props, state, header) } diff --git a/auth-web/src/components/pay/LinkedShortNameTable.vue b/auth-web/src/components/pay/LinkedShortNameTable.vue new file mode 100644 index 0000000000..780159f70a --- /dev/null +++ b/auth-web/src/components/pay/LinkedShortNameTable.vue @@ -0,0 +1,184 @@ + + + + diff --git a/auth-web/src/components/pay/ShortNameLookup.vue b/auth-web/src/components/pay/ShortNameLookup.vue new file mode 100644 index 0000000000..1c2a777b03 --- /dev/null +++ b/auth-web/src/components/pay/ShortNameLookup.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/auth-web/src/components/pay/UnlinkedShortNameTable.vue b/auth-web/src/components/pay/UnlinkedShortNameTable.vue new file mode 100644 index 0000000000..5b031f846b --- /dev/null +++ b/auth-web/src/components/pay/UnlinkedShortNameTable.vue @@ -0,0 +1,320 @@ + + + + diff --git a/auth-web/src/composables/short-name-table-factory.ts b/auth-web/src/composables/short-name-table-factory.ts new file mode 100644 index 0000000000..94ef7b8718 --- /dev/null +++ b/auth-web/src/composables/short-name-table-factory.ts @@ -0,0 +1,88 @@ +import { LinkedShortNameState, UnlinkedShortNameState } from '@/models/pay/short-name' +import PaymentService from '@/services/payment.services' + +/* Not using a global state here, state can be passed as a reactive object through to the factory. */ +export function useShortNameTable (tableState: LinkedShortNameState | UnlinkedShortNameState, emit) { + const state = tableState + + /* Always includes state, which differes from the Affiliation table. */ + function handleFilters (filterField?: string, value?: any): void { + state.loading = true + if (filterField) { + state.filters.pageNumber = 1 + if (filterField === 'depositDate') { + state.filters.filterPayload.transactionStartDate = value.startDate + state.filters.filterPayload.transactionEndDate = value.endDate + } else { + state.filters.filterPayload[filterField] = value + } + } + let filtersActive = false + for (const key in state.filters.filterPayload) { + // Always send state, don't count it as active filters. + if (key === 'state') { + continue + } + if (key === 'dateFilter') { + if (state.filters.filterPayload[key].endDate) filtersActive = true + } else if (state.filters.filterPayload[key]) filtersActive = true + if (filtersActive) break + } + state.filters.isActive = filtersActive + } + + /* This is also called inside of the HeaderFilter component inside of the BaseVDataTable component + * Parts of this is duplicated inside of the other datatable components. + */ + async function loadTableData (filterField?: string, value?: any, appendToResults = false): Promise { + handleFilters(filterField, value) + try { + const response = await PaymentService.getEFTShortNames(state.filters) + if (response?.data) { + /* We use appendToResults for infinite scroll, so we keep the existing results. */ + state.results = appendToResults ? state.results.concat(response.data.items) : response.data.items + state.totalResults = response.data.total + emit('shortname-state-total', response.data.stateTotal) + } else { + throw new Error('No response from getEFTShortNames') + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to getEFTShortNames list.', error) + } + state.loading = false + } + + /* This cannot be done inside of the BaseDataTable component because it manupulates the state outside of it. */ + function updateFilter (filterField?: string, value?: any) : void { + if (filterField) { + if (value) { + state.filters.filterPayload[filterField] = value + state.filters.isActive = true + } else { + delete state.filters.filterPayload[filterField] + } + } + /* We always send over state in the filter payload. */ + if (Object.keys(state.filters.filterPayload).length === 1) { + state.filters.isActive = false + } else { + state.filters.isActive = true + } + } + + /* Instead of slicing up the results, we handle the results inside of this function. */ + async function infiniteScrollCallback () { + if (state.totalResults < (state.filters.pageLimit * state.filters.pageNumber)) return true + state.filters.pageNumber++ + await loadTableData(null, null, true) + return false + } + + return { + infiniteScrollCallback, + handleFilters, + loadTableData, + updateFilter + } +} diff --git a/auth-web/src/models/pay/short-name.ts b/auth-web/src/models/pay/short-name.ts new file mode 100644 index 0000000000..0358494336 --- /dev/null +++ b/auth-web/src/models/pay/short-name.ts @@ -0,0 +1,71 @@ +import { DataOptions } from 'vuetify' +import { ShortNameStatus } from '@/util/constants' + +export interface LinkedShortNameFilterParams { + isActive: boolean + pageNumber: number + pageLimit: number + filterPayload: { + accountName?: string + shortName?: string + accountBranch?: string + accountId?: string + state: ShortNameStatus + transactionStartDate: string + transactionEndDate: string + } +} + +export interface LinkedShortNameResults { + accountName?: string + shortName?: string + accountBranch?: string + accountId?: string + id: number +} + +export interface LinkedShortNameState { + results: LinkedShortNameResults[] + totalResults: number + loading: boolean + filters: LinkedShortNameFilterParams + actionDropdown: any[] + options: DataOptions +} + +export interface UnlinkedShortNameFilterParams { + isActive: boolean + pageNumber: number + pageLimit: number + filterPayload: { + shortName?: string + depositDate?: string + depositAmount?: number + state: ShortNameStatus + transactionStartDate: string + transactionEndDate: string + } +} + +export interface UnlinkedShortNameResults { + shortName?: string + depositDate?: string + depositAmount?: number + id: number +} + +export interface UnlinkedShortNameState { + results: UnlinkedShortNameResults[] + totalResults: number + loading: boolean + filters: UnlinkedShortNameFilterParams + actionDropdown: any[] + options: DataOptions + shortNameLookupKey: number + dateRangeReset: number + clearFiltersTrigger: number + selectedShortName: object + showDatePicker: boolean + dateRangeSelected: boolean + dateRangeText: string +} diff --git a/auth-web/src/routes/router.ts b/auth-web/src/routes/router.ts index 42101138a5..651ccefb64 100644 --- a/auth-web/src/routes/router.ts +++ b/auth-web/src/routes/router.ts @@ -61,6 +61,7 @@ import { RouteConfig } from 'vue-router' import SetupAccountSuccessView from '@/views/auth/staff/SetupAccountSuccessView.vue' import SetupAccountView from '@/views/auth/staff/SetupAccountView.vue' import SetupGovmAccountView from '@/views/auth/staff/SetupGovmAccountView.vue' +import ShortNameMappingView from '@/views/pay/ShortNameMappingView.vue' import SigninView from '@/views/auth/SigninView.vue' import SignoutView from '@/views/auth/SignoutView.vue' import StaffActiveAccountsTable from '@/components/auth/staff/account-management/StaffActiveAccountsTable.vue' @@ -795,6 +796,13 @@ export function getRoutes (): RouteConfig[] { props: true, meta: { requiresAuth: true } }, + { + path: '/pay/manage-shortnames', + name: 'manage-shortnames', + component: ShortNameMappingView, + meta: { requiresAuth: true, allowedRoles: [Role.Staff] }, // TODO rewire this in #19673 + props: true + }, { path: '*', name: 'notfound', component: PageNotFound } ] diff --git a/auth-web/src/services/payment.services.ts b/auth-web/src/services/payment.services.ts index d86841f493..859b42717f 100644 --- a/auth-web/src/services/payment.services.ts +++ b/auth-web/src/services/payment.services.ts @@ -12,6 +12,7 @@ import { TransactionFilter, TransactionFilterParams, TransactionListResponse } f import { AxiosPromise } from 'axios' import ConfigHelper from '@/util/config-helper' +import { LinkedShortNameFilterParams } from '@/models/pay/short-name' import { Payment } from '@/models/Payment' import { PaymentTypes } from '@/util/constants' import { axios } from '@/util/http-util' @@ -203,4 +204,27 @@ export default class PaymentService { const body = {} return axios.post(url, body, { headers, responseType: 'blob' as 'json' }) } + + static getEFTShortNames (filterParams: LinkedShortNameFilterParams, viewAll = false): AxiosPromise { + const params = new URLSearchParams() + if (filterParams.pageNumber) { + params.append('page', filterParams.pageNumber.toString()) + } + if (filterParams.pageLimit) { + params.append('limit', filterParams.pageLimit.toString()) + } + if (viewAll) { + params.append('viewAll', `${viewAll}`) + } + + if (filterParams.filterPayload) { + for (const [key, value] of Object.entries(filterParams.filterPayload)) { + if (value) { + params.append(key, value) + } + } + } + + return axios.get(`${ConfigHelper.getPayAPIURL()}/eft-shortnames?${params.toString()}`) + } } diff --git a/auth-web/src/util/common-util.ts b/auth-web/src/util/common-util.ts index a771fa45c3..3da465703e 100644 --- a/auth-web/src/util/common-util.ts +++ b/auth-web/src/util/common-util.ts @@ -109,7 +109,7 @@ export default class CommonUtils { } // Formatting date in the desired format for displaying in the template - static formatDisplayDate (date: Date, format?: string) { + static formatDisplayDate (date: Date | string, format?: string) { // not working in CI (getting UTC datetime) return (date) ? moment(date.toLocaleString('en-US', { timeZone: 'America/Vancouver' })) .format(format || 'YYYY-MM-DD') : '' diff --git a/auth-web/src/util/constants.ts b/auth-web/src/util/constants.ts index 0e4528cbf9..3eabf4f478 100644 --- a/auth-web/src/util/constants.ts +++ b/auth-web/src/util/constants.ts @@ -607,3 +607,8 @@ export enum AffiliationInvitationType { REQUEST = 'REQUEST', EMAIL = 'EMAIL' } + +export enum ShortNameStatus { + LINKED = 'LINKED', + UNLINKED = 'UNLINKED' +} diff --git a/auth-web/src/util/debounce.ts b/auth-web/src/util/debounce.ts index 50b759343d..0bd139bb5f 100644 --- a/auth-web/src/util/debounce.ts +++ b/auth-web/src/util/debounce.ts @@ -1,4 +1,4 @@ -export function debounce void>(fn: F, delay = 300) { +export function debounce void | Promise>(fn: F, delay = 300) { let timeoutID: number = null return function (this: any, ...args: any[]) { clearTimeout(timeoutID) diff --git a/auth-web/src/views/auth/staff/StaffDashboardView.vue b/auth-web/src/views/auth/staff/StaffDashboardView.vue index 3b4ddf9e55..cc1bb127b8 100644 --- a/auth-web/src/views/auth/staff/StaffDashboardView.vue +++ b/auth-web/src/views/auth/staff/StaffDashboardView.vue @@ -152,7 +152,7 @@ - - + - - + - - + diff --git a/auth-web/src/views/pay/ShortNameMappingView.vue b/auth-web/src/views/pay/ShortNameMappingView.vue new file mode 100644 index 0000000000..8f38d452b8 --- /dev/null +++ b/auth-web/src/views/pay/ShortNameMappingView.vue @@ -0,0 +1,145 @@ + + + + diff --git a/auth-web/tests/unit/components/LinkedShortNameTable.spec.ts b/auth-web/tests/unit/components/LinkedShortNameTable.spec.ts new file mode 100644 index 0000000000..7782ac375d --- /dev/null +++ b/auth-web/tests/unit/components/LinkedShortNameTable.spec.ts @@ -0,0 +1,109 @@ +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' +import { BaseVDataTable } from '@/components' +import LinkedShortNameTableVue from '@/components/pay/LinkedShortNameTable.vue' +import { VueConstructor } from 'vue' +import Vuetify from 'vuetify' +import { axios } from '@/util/http-util' +import { baseVdataTable } from './../test-utils/test-data/baseVdata' +import { setupIntersectionObserverMock } from '../util/helper-functions' +import sinon from 'sinon' + +sessionStorage.setItem('AUTH_API_CONFIG', JSON.stringify({ + AUTH_API_URL: 'https://localhost:8080/api/v1/11', + PAY_API_URL: 'https://pay-api-dev.apps.silver.devops.gov.bc.ca/api/v1' +})) + +const vuetify = new Vuetify({}) +// Selectors +const { header, headerTitles, itemRow, itemCell } = baseVdataTable +const headers = ['Bank Short Name', 'Account Name', 'Branch Name', 'Account Number', 'Actions'] + +describe('LinkedShortNameTable.vue', () => { + setupIntersectionObserverMock() + let wrapper: Wrapper + let sandbox: any + let localVue: VueConstructor + let linkedShortNameResponse: any + + beforeEach(async () => { + localVue = createLocalVue() + linkedShortNameResponse = { + items: [ + { + shortName: 'RCPV', + accountName: 'RCPV', + accountBranch: 'Saanich', + accountId: '3199', + id: 1 + }, + { + shortName: 'SHORT NAME', + accountName: 'SUPER ACCOUNT', + accountBranch: 'OUT THERE', + accountId: '3135', + id: 2 + }, + { + shortName: 'little name', + accountName: 'BIG ACCOUNT', + accountBranch: 'VICTORIA', + accountId: '6000', + id: 3 + }, + { + shortName: 'WOW', + accountName: 'BOO', + accountBranch: '', + accountId: '911', + id: 4 + } + ], + total: 4 + } + + sandbox = sinon.createSandbox() + const get = sandbox.stub(axios, 'get') + get.returns(new Promise(resolve => resolve({ data: linkedShortNameResponse }))) + + wrapper = mount(LinkedShortNameTableVue, { + localVue, + vuetify + }) + await wrapper.vm.$nextTick() + }) + + afterEach(() => { + wrapper.destroy() + sessionStorage.clear() + sandbox.restore() + + vi.resetModules() + vi.clearAllMocks() + }) + + it('Renders linked short name table with correct contents', async () => { + expect(wrapper.find('#table-title-cell').text()).toContain('Linked Bank Short Names (4)') + + // verify table + expect(wrapper.findComponent(BaseVDataTable).exists()).toBe(true) + expect(wrapper.findComponent(BaseVDataTable).find(header).exists()).toBe(true) + expect(wrapper.find('#linked-bank-short-names').exists()).toBe(true) + expect(wrapper.find('.v-data-table__wrapper').exists()).toBe(true) + const titles = wrapper.findComponent(BaseVDataTable).findAll(headerTitles) + expect(titles.length).toBe(headers.length) + for (let i = 0; i < headers.length; i++) { + expect(titles.at(i).text()).toBe(headers[i]) + } + + // verify data + const itemRows = wrapper.findComponent(BaseVDataTable).findAll(itemRow) + expect(itemRows.length).toBe(linkedShortNameResponse.items.length) + for (let i = 0; i < linkedShortNameResponse.items.length; i++) { + const columns = itemRows.at(i).findAll(itemCell) + expect(columns.at(0).text()).toBe(linkedShortNameResponse.items[i].shortName) + expect(columns.at(1).text()).toBe(linkedShortNameResponse.items[i].accountName) + expect(columns.at(2).text()).toBe(linkedShortNameResponse.items[i].accountBranch) + expect(columns.at(3).text()).toBe(linkedShortNameResponse.items[i].accountId) + } + }) +}) diff --git a/auth-web/tests/unit/components/UnlinkedShortNameTable.spec.ts b/auth-web/tests/unit/components/UnlinkedShortNameTable.spec.ts new file mode 100644 index 0000000000..4a45e53d37 --- /dev/null +++ b/auth-web/tests/unit/components/UnlinkedShortNameTable.spec.ts @@ -0,0 +1,120 @@ +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' +import { BaseVDataTable } from '@/components' +import CommonUtils from '@/util/common-util' +import UnlinkedShortNameTableVue from '@/components/pay/UnlinkedShortNameTable.vue' +import { VueConstructor } from 'vue' +import Vuetify from 'vuetify' +import { axios } from '@/util/http-util' +import { baseVdataTable } from './../test-utils/test-data/baseVdata' +import { setupIntersectionObserverMock } from '../util/helper-functions' +import sinon from 'sinon' + +sessionStorage.setItem('AUTH_API_CONFIG', JSON.stringify({ + AUTH_API_URL: 'https://localhost:8080/api/v1/11', + PAY_API_URL: 'https://pay-api-dev.apps.silver.devops.gov.bc.ca/api/v1' +})) + +const vuetify = new Vuetify({}) +// Selectors +const header = baseVdataTable.header +const headerTitles = baseVdataTable.headerTitles +const itemRow = baseVdataTable.itemRow +const itemCell = baseVdataTable.itemCell + +const headers = [ + 'Bank Short Name', + 'Initial Payment Received Date', + 'Initial Payment Amount', + 'Actions' +] + +describe('UnlinkedShortNameTable.vue', () => { + setupIntersectionObserverMock() + let wrapper: Wrapper + let localVue: VueConstructor + let unlinkedShortNameResponse: any + + beforeEach(async () => { + localVue = createLocalVue() + unlinkedShortNameResponse = { + items: [ + { + shortName: 'TST1', + depositDate: '2024-01-28T10:00:00', + depositAmount: 5, + id: 1 + }, + { + shortName: 'TST2', + depositDate: '2024-01-29T10:00:00', + depositAmount: 50, + id: 2 + }, + { + shortName: 'TST3', + depositDate: '2024-01-30T10:00:00', + depositAmount: 133.33, + id: 3 + }, + { + shortName: 'TST4', + depositDate: '2024-01-31T10:00:00', + depositAmount: 121.21, + id: 4 + }, + { + shortName: 'TST5', + depositDate: '2024-02-01T10:00:00', + depositAmount: 333.33, + id: 5 + } + ], + total: 5 + } + + const sandbox = sinon.createSandbox() + const get = sandbox.stub(axios, 'get') + get.returns(new Promise(resolve => resolve({ data: unlinkedShortNameResponse }))) + + wrapper = mount(UnlinkedShortNameTableVue, { + localVue, + vuetify + }) + await wrapper.vm.$nextTick() + }) + + afterEach(() => { + wrapper.destroy() + sessionStorage.clear() + + vi.resetModules() + vi.clearAllMocks() + }) + + it('Renders unlinked short name table with correct contents', async () => { + expect(wrapper.find('#table-title-cell').text()).toContain('Unlinked Bank Short Names (5)') + + // verify table + expect(wrapper.findComponent(BaseVDataTable).exists()).toBe(true) + expect(wrapper.findComponent(BaseVDataTable).find(header).exists()).toBe(true) + expect(wrapper.find('#unlinked-bank-short-names').exists()).toBe(true) + expect(wrapper.find('.v-data-table__wrapper').exists()).toBe(true) + const titles = wrapper.findComponent(BaseVDataTable).findAll(headerTitles) + expect(titles.length).toBe(headers.length) + for (let i = 0; i < headers.length; i++) { + expect(titles.at(i).text()).toBe(headers[i]) + } + + // verify data + const itemRows = wrapper.findComponent(BaseVDataTable).findAll(itemRow) + expect(itemRows.length).toBe(unlinkedShortNameResponse.items.length) + for (let i = 0; i < unlinkedShortNameResponse.items.length; i++) { + const columns = itemRows.at(i).findAll(itemCell) + expect(columns.at(0).text()).toBe(unlinkedShortNameResponse.items[i].shortName) + expect(columns.at(1).text()).toBe( + CommonUtils.formatDisplayDate(unlinkedShortNameResponse.items[i].depositDate, 'MMMM DD, YYYY')) + expect(columns.at(2).text()).toBe( + CommonUtils.formatAmount(unlinkedShortNameResponse.items[i].depositAmount)) + } + }) +})