From 210d9782793e902c4908e505fd41c2925228d1ab Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 3 Nov 2024 20:59:00 +0800 Subject: [PATCH] support filtering transactions in import transaction dialog --- src/locales/en.json | 1 + src/locales/zh_Hans.json | 1 + .../list/dialogs/ImportDialog.vue | 389 +++++++++++++++++- 3 files changed, 377 insertions(+), 14 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 4a1b0724..fa94557f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1526,6 +1526,7 @@ "Check and Modify Your Data": "Check and Modify Your Data", "Data Import Completed": "Data Import Completed", "File Type": "File Type", + "Filter Description": "Filter Description", "How to export this file?": "How to export this file?", "ezbookkeeping Data Export File": "ezbookkeeping Data Export File", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) File", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 3672651d..dd158d57 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1526,6 +1526,7 @@ "Check and Modify Your Data": "检查及修改您的数据", "Data Import Completed": "数据导入完成", "File Type": "文件类型", + "Filter Description": "过滤描述", "How to export this file?": "如何导出该文件?", "ezbookkeeping Data Export File": "ezbookkeeping 数据导出文件", "Open Financial Exchange (OFX) File": "开放式金融交换 (OFX) 文件", diff --git a/src/views/desktop/transactions/list/dialogs/ImportDialog.vue b/src/views/desktop/transactions/list/dialogs/ImportDialog.vue index 5d6b4e85..022c8df9 100644 --- a/src/views/desktop/transactions/list/dialogs/ImportDialog.vue +++ b/src/views/desktop/transactions/list/dialogs/ImportDialog.vue @@ -7,6 +7,93 @@

{{ $t('Import Transactions') }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -125,6 +212,8 @@ :height="importTransactionsTableHeight" :headers="importTransactionHeaders" :items="importTransactions" + :search="JSON.stringify(filters)" + :custom-filter="importTransactionsFilter" :no-data-text="$t('No data to import')" v-model:items-per-page="countPerPage" v-model:page="currentPage" @@ -467,6 +556,36 @@ + + + + + + + +
+ {{ $t('OK') }} + {{ $t('Cancel') }} +
+
+
+
+ + @@ -489,7 +608,12 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.js'; import categoryConstants from '@/consts/category.js'; import transactionConstants from '@/consts/transaction.js'; -import { getNameByKeyValue } from '@/lib/common.js'; +import { + isString, + isNumber, + getNameByKeyValue, + objectFieldToArrayItem +} from '@/lib/common.js'; import { isFileExtensionSupported } from '@/lib/file.js'; import { generateRandomUUID } from '@/lib/misc.js'; import logger from '@/lib/logger.js'; @@ -506,12 +630,13 @@ import { } from '@/lib/category.js'; import { + mdiFilterOutline, + mdiCheck, mdiDotsVertical, mdiHelpCircleOutline, mdiFindReplace, mdiClose, mdiArrowRight, - mdiCheck, mdiSelectAll, mdiSelect, mdiSelectInverse, @@ -541,14 +666,28 @@ export default { importTransactions: null, editingTransaction: null, editingTags: [], + filters: { + minDatetime: null, + maxDatetime: null, + transactionType: null, + category: null, + account: null, + tag: null, + description: null + }, currentPage: 1, countPerPage: 10, importedCount: null, + showCustomDateRangeDialog: false, + showCustomDescriptionDialog: false, + currentDescriptionFilterValue: null, loading: true, submitting: false, resolve: null, reject: null, icons: { + filter: mdiFilterOutline, + checked: mdiCheck, more: mdiDotsVertical, document: mdiHelpCircleOutline, replace: mdiFindReplace, @@ -766,7 +905,36 @@ export default { return 1; } - return Math.ceil(this.importTransactions.length / this.countPerPage); + let count = 0; + + for (let i = 0; i < this.importTransactions.length; i++) { + if (this.isTransactionDisplayed(this.importTransactions[i])) { + count++; + } + } + + return Math.ceil(count / this.countPerPage); + }, + currentPageTransactions() { + const ret = []; + const previousCount = Math.max(0, (this.currentPage - 1) * this.countPerPage); + let count = 0; + + for (let i = 0; i < this.importTransactions.length; i++) { + if (ret.length >= this.countPerPage) { + break; + } + + if (this.isTransactionDisplayed(this.importTransactions[i])) { + if (count >= previousCount) { + ret.push(this.importTransactions[i]); + } + + count++; + } + } + + return ret; }, selectedImportTransactionCount() { let count = 0; @@ -833,6 +1001,15 @@ export default { return this.selectedImportTransactionCount === this.importTransactions.length; } }, + allUsedCategoryNames() { + return this.getAllUsedCategoryNames(); + }, + allUsedAccountNames() { + return this.getAllUsedAccountNames(); + }, + allUsedTagNames() { + return this.getAllUsedTagNames(); + }, allInvalidExpenseCategoryNames() { return this.getCurrentInvalidCategoryNames(this.allTransactionTypes.Expense); }, @@ -847,6 +1024,16 @@ export default { }, allInvalidTransactionTagNames() { return this.getCurrentInvalidTagNames(); + }, + displayFilterCustomDateRange() { + if (this.filters.minDatetime === null || this.filters.maxDatetime === null) { + return ''; + } + + const minDisplayTime = this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.filters.minDatetime); + const maxDisplayTime = this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.filters.maxDatetime); + + return `${minDisplayTime} - ${maxDisplayTime}` } }, watch: { @@ -884,6 +1071,13 @@ export default { self.importTransactions = null; self.editingTransaction = null; self.editingTags = []; + self.filters.minDatetime = null; + self.filters.maxDatetime = null; + self.filters.transactionType = null; + self.filters.category = null; + self.filters.account = null; + self.filters.tag = null; + self.filters.description = null; self.currentPage = 1; self.countPerPage = 10; self.showState = true; @@ -954,6 +1148,7 @@ export default { transaction.valid = self.isTransactionValid(transaction); transaction.actualCategoryName = transaction.originalCategoryName; transaction.actualSourceAccountName = transaction.originalSourceAccountName; + transaction.actualDestinationAccountName = transaction.originalDestinationAccountName; } } @@ -1045,48 +1240,156 @@ export default { this.showState = false; }, + changeCustomDateFilter(minTime, maxTime) { + this.filters.minDatetime = minTime; + this.filters.maxDatetime = maxTime; + this.showCustomDateRangeDialog = false; + }, + importTransactionsFilter(value, query, item) { + if (!item || !item.raw) { + return false; + } + + return this.isTransactionDisplayed(item.raw); + }, + isTransactionDisplayed(transaction) { + if (isNumber(this.filters.minDatetime) && isNumber(this.filters.maxDatetime) && (transaction.time < this.filters.minDatetime || transaction.time > this.filters.maxDatetime)) { + return false; + } + + if (isNumber(this.filters.transactionType) && transaction.type !== this.filters.transactionType) { + return false; + } + + if (isString(this.filters.category)) { + if (this.filters.category === '' && transaction.actualCategoryName !== '') { + return false; + } else if (this.filters.category !== '' && transaction.actualCategoryName !== this.filters.category) { + return false; + } + } else if (this.filters.category === undefined) { + if (transaction.type !== this.allTransactionTypes.ModifyBalance && transaction.categoryId && transaction.categoryId !== '0') { + return false; + } + } + + if (isString(this.filters.account)) { + if (this.filters.account === '' && (transaction.actualSourceAccountName !== '' || transaction.actualDestinationAccountName !== '')) { + return false; + } else if (this.filters.account !== '' && transaction.actualSourceAccountName !== this.filters.account && transaction.actualDestinationAccountName !== this.filters.account) { + return false; + } + } else if (this.filters.account === undefined) { + if (transaction.type !== this.allTransactionTypes.Transfer && transaction.sourceAccountId && transaction.sourceAccountId !== '0') { + return false; + } else if (transaction.type === this.allTransactionTypes.Transfer && transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.destinationAccountId && transaction.destinationAccountId !== '0') { + return false; + } + } + + if (isString(this.filters.tag)) { + if (this.filters.tag === '' && transaction.tagIds && transaction.tagIds.length) { + return false; + } else if (this.filters.tag !== '') { + let hasTagName = false; + + if (transaction.tagIds && transaction.tagIds.length) { + for (let i = 0; i < transaction.tagIds.length; i++) { + const tagId = transaction.tagIds[i]; + let tagName = transaction.originalTagNames ? transaction.originalTagNames[i] : ""; + + if (tagId && tagId !== '0' && this.allTagsMap[tagId] && this.allTagsMap[tagId].name) { + tagName = this.allTagsMap[tagId].name; + } + + if (tagName === this.filters.tag) { + hasTagName = true; + break; + } + } + } + + if (!hasTagName) { + return false; + } + } + } else if (this.filters.tag === undefined) { + if (transaction.tagIds && transaction.tagIds.length) { + let hasInvalidTag = false; + + for (let i = 0; i < transaction.tagIds.length; i++) { + if (!transaction.tagIds[i] || transaction.tagIds[i] === '0') { + hasInvalidTag = true; + break; + } + } + + if (!hasInvalidTag) { + return false; + } + } else { + return false; + } + } + + if (isString(this.filters.description)) { + if (this.filters.description === '' && transaction.comment !== '') { + return false; + } else if (this.filters.description !== '' && transaction.comment.indexOf(this.filters.description) < 0) { + return false; + } + } + + return true; + }, selectAllValid() { for (let i = 0; i < this.importTransactions.length; i++) { - if (this.importTransactions[i].valid) { + if (this.importTransactions[i].valid && this.isTransactionDisplayed(this.importTransactions[i])) { this.importTransactions[i].selected = true; } } }, selectAllInvalid() { for (let i = 0; i < this.importTransactions.length; i++) { - if (!this.importTransactions[i].valid) { + if (!this.importTransactions[i].valid && this.isTransactionDisplayed(this.importTransactions[i])) { this.importTransactions[i].selected = true; } } }, selectAll() { for (let i = 0; i < this.importTransactions.length; i++) { - this.importTransactions[i].selected = true; + if (this.isTransactionDisplayed(this.importTransactions[i])) { + this.importTransactions[i].selected = true; + } } }, selectNone() { for (let i = 0; i < this.importTransactions.length; i++) { - this.importTransactions[i].selected = false; + if (this.isTransactionDisplayed(this.importTransactions[i])) { + this.importTransactions[i].selected = false; + } } }, selectInvert() { for (let i = 0; i < this.importTransactions.length; i++) { - this.importTransactions[i].selected = !this.importTransactions[i].selected; + if (this.isTransactionDisplayed(this.importTransactions[i])) { + this.importTransactions[i].selected = !this.importTransactions[i].selected; + } } }, selectAllInThisPage() { - for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { - this.importTransactions[i].selected = true; + for (let i = 0; i < this.currentPageTransactions.length; i++) { + this.currentPageTransactions[i].selected = true; } }, selectNoneInThisPage() { - for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { - this.importTransactions[i].selected = false; + for (let i = 0; i < this.currentPageTransactions.length; i++) { + this.currentPageTransactions[i].selected = false; } }, selectInvertInThisPage() { - for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { - this.importTransactions[i].selected = !this.importTransactions[i].selected; + for (let i = 0; i < this.currentPageTransactions.length; i++) { + this.currentPageTransactions[i].selected = !this.currentPageTransactions[i].selected; } }, editTransaction(transaction) { @@ -1113,6 +1416,10 @@ export default { if (transaction.sourceAccountId && this.allAccountsMap[transaction.sourceAccountId]) { transaction.actualSourceAccountName = this.allAccountsMap[transaction.sourceAccountId].name; } + + if (transaction.destinationAccountId && this.allAccountsMap[transaction.destinationAccountId]) { + transaction.actualDestinationAccountName = this.allAccountsMap[transaction.destinationAccountId].name; + } }, showBatchReplaceDialog(type) { const self = this; @@ -1261,6 +1568,60 @@ export default { } }); }, + getAllUsedCategoryNames() { + const categoryNames = {}; + + for (let i = 0; i < this.importTransactions.length; i++) { + const transaction = this.importTransactions[i]; + + if (transaction.actualCategoryName && transaction.actualCategoryName !== '') { + categoryNames[transaction.actualCategoryName] = true; + } + } + + return objectFieldToArrayItem(categoryNames); + }, + getAllUsedAccountNames() { + const accountNames = {}; + + for (let i = 0; i < this.importTransactions.length; i++) { + const transaction = this.importTransactions[i]; + + if (transaction.actualSourceAccountName && transaction.actualSourceAccountName !== '') { + accountNames[transaction.actualSourceAccountName] = true; + } + + if (transaction.actualDestinationAccountName && transaction.actualDestinationAccountName !== '') { + accountNames[transaction.actualDestinationAccountName] = true; + } + } + + return objectFieldToArrayItem(accountNames); + }, + getAllUsedTagNames(){ + const tagNames = {}; + + for (let i = 0; i < this.importTransactions.length; i++) { + const transaction = this.importTransactions[i]; + + if (!transaction.tagIds || !transaction.originalTagNames) { + continue; + } + + for (let j = 0; j < transaction.tagIds.length; j++) { + const tagId = transaction.tagIds[j]; + const originalTagName = transaction.originalTagNames[j]; + + if (tagId && tagId !== '0' && this.allTagsMap[tagId] && this.allTagsMap[tagId].name) { + tagNames[this.allTagsMap[tagId].name] = true; + } else if (originalTagName) { + tagNames[originalTagName] = true; + } + } + } + + return objectFieldToArrayItem(tagNames); + }, getCurrentInvalidCategoryNames(transactionType) { const invalidCategoryNames = {}; const invalidCategories = [];