support batch replace category / account / tag in import transaction dialog

This commit is contained in:
MaysWind
2024-09-23 23:47:02 +08:00
parent 29781bbac4
commit d32cd793d0
4 changed files with 167 additions and 31 deletions
+5 -1
View File
@@ -1509,7 +1509,11 @@
"No data to import": "No data to import", "No data to import": "No data to import",
"Cannot import invalid transactions": "Cannot import invalid transactions", "Cannot import invalid transactions": "Cannot import invalid transactions",
"Unable to parse import file": "Unable to parse import file", "Unable to parse import file": "Unable to parse import file",
"Batch Replace": "Batch Replace", "Batch Replace Selected Expense Categories": "Batch Replace Selected Expense Categories",
"Batch Replace Selected Income Categories": "Batch Replace Selected Income Categories",
"Batch Replace Selected Transfer Categories": "Batch Replace Selected Transfer Categories",
"Batch Replace Selected Accounts": "Batch Replace Selected Accounts",
"Batch Replace Selected Destination Accounts": "Batch Replace Selected Destination Accounts",
"Replace Invalid Expense Categories": "Replace Invalid Expense Categories", "Replace Invalid Expense Categories": "Replace Invalid Expense Categories",
"Replace Invalid Income Categories": "Replace Invalid Income Categories", "Replace Invalid Income Categories": "Replace Invalid Income Categories",
"Replace Invalid Transfer Categories": "Replace Invalid Transfer Categories", "Replace Invalid Transfer Categories": "Replace Invalid Transfer Categories",
+5 -1
View File
@@ -1509,7 +1509,11 @@
"No data to import": "没有可以导入的数据", "No data to import": "没有可以导入的数据",
"Cannot import invalid transactions": "不能导入无效的交易", "Cannot import invalid transactions": "不能导入无效的交易",
"Unable to parse import file": "无法解析导入的文件", "Unable to parse import file": "无法解析导入的文件",
"Batch Replace": "批量替换", "Batch Replace Selected Expense Categories": "批量替换选中的支出分类",
"Batch Replace Selected Income Categories": "批量替换选中的收入分类",
"Batch Replace Selected Transfer Categories": "批量替换选中的转账分类",
"Batch Replace Selected Accounts": "批量替换选中的账户",
"Batch Replace Selected Destination Accounts": "批量替换选中的目标账户",
"Replace Invalid Expense Categories": "替换无效的支出分类", "Replace Invalid Expense Categories": "替换无效的支出分类",
"Replace Invalid Income Categories": "替换无效的收入分类", "Replace Invalid Income Categories": "替换无效的收入分类",
"Replace Invalid Transfer Categories": "替换无效的转账分类", "Replace Invalid Transfer Categories": "替换无效的转账分类",
@@ -3,16 +3,21 @@
<v-card class="pa-2 pa-sm-4 pa-md-4"> <v-card class="pa-2 pa-sm-4 pa-md-4">
<template #title> <template #title>
<div class="d-flex align-center justify-center"> <div class="d-flex align-center justify-center">
<h4 class="text-h4" v-if="type === 'expenseCategory'">{{ $t('Replace Invalid Expense Categories') }}</h4> <h4 class="text-h4" v-if="mode === 'batchReplace' && type === 'expenseCategory'">{{ $t('Batch Replace Selected Expense Categories') }}</h4>
<h4 class="text-h4" v-if="type === 'incomeCategory'">{{ $t('Replace Invalid Income Categories') }}</h4> <h4 class="text-h4" v-if="mode === 'batchReplace' && type === 'incomeCategory'">{{ $t('Batch Replace Selected Income Categories') }}</h4>
<h4 class="text-h4" v-if="type === 'transferCategory'">{{ $t('Replace Invalid Transfer Categories') }}</h4> <h4 class="text-h4" v-if="mode === 'batchReplace' && type === 'transferCategory'">{{ $t('Batch Replace Selected Transfer Categories') }}</h4>
<h4 class="text-h4" v-if="type === 'account'">{{ $t('Replace Invalid Accounts') }}</h4> <h4 class="text-h4" v-if="mode === 'batchReplace' && type === 'account'">{{ $t('Batch Replace Selected Accounts') }}</h4>
<h4 class="text-h4" v-if="type === 'tag'">{{ $t('Replace Invalid Transaction Tags') }}</h4> <h4 class="text-h4" v-if="mode === 'batchReplace' && type === 'destinationAccount'">{{ $t('Batch Replace Selected Destination Accounts') }}</h4>
<h4 class="text-h4" v-if="mode === 'replaceInvalidItems' && type === 'expenseCategory'">{{ $t('Replace Invalid Expense Categories') }}</h4>
<h4 class="text-h4" v-if="mode === 'replaceInvalidItems' && type === 'incomeCategory'">{{ $t('Replace Invalid Income Categories') }}</h4>
<h4 class="text-h4" v-if="mode === 'replaceInvalidItems' && type === 'transferCategory'">{{ $t('Replace Invalid Transfer Categories') }}</h4>
<h4 class="text-h4" v-if="mode === 'replaceInvalidItems' && type === 'account'">{{ $t('Replace Invalid Accounts') }}</h4>
<h4 class="text-h4" v-if="mode === 'replaceInvalidItems' && type === 'tag'">{{ $t('Replace Invalid Transaction Tags') }}</h4>
</div> </div>
</template> </template>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory'"> <v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory'">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12" v-if="mode === 'replaceInvalidItems'">
<v-autocomplete <v-autocomplete
item-title="name" item-title="name"
item-value="value" item-value="value"
@@ -76,9 +81,9 @@
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'account'"> <v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'account' || type === 'destinationAccount'">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12" v-if="mode === 'replaceInvalidItems'">
<v-autocomplete <v-autocomplete
item-title="name" item-title="name"
item-value="value" item-value="value"
@@ -111,7 +116,7 @@
</v-card-text> </v-card-text>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'tag'"> <v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'tag'">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12" v-if="mode === 'replaceInvalidItems'">
<v-autocomplete <v-autocomplete
item-title="name" item-title="name"
item-value="value" item-value="value"
@@ -157,7 +162,7 @@
</v-card-text> </v-card-text>
<v-card-text class="overflow-y-visible"> <v-card-text class="overflow-y-visible">
<div class="w-100 d-flex justify-center gap-4"> <div class="w-100 d-flex justify-center gap-4">
<v-btn :disabled="(!sourceItem && sourceItem !== '') || (!targetItem && targetItem !== '')" @click="confirm">{{ $t('OK') }}</v-btn> <v-btn :disabled="(mode === 'replaceInvalidItems' && !sourceItem && sourceItem !== '') || (!targetItem && targetItem !== '')" @click="confirm">{{ $t('OK') }}</v-btn>
<v-btn color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn> <v-btn color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn>
</div> </div>
</v-card-text> </v-card-text>
@@ -198,6 +203,7 @@ export default {
data() { data() {
return { return {
showState: false, showState: false,
mode: '',
type: '', type: '',
invalidItems: [], invalidItems: [],
sourceItem: null, sourceItem: null,
@@ -264,9 +270,16 @@ export default {
methods: { methods: {
open(options) { open(options) {
const self = this; const self = this;
self.mode = options.mode;
self.type = options.type; self.type = options.type;
self.invalidItems = options.invalidItems;
self.sourceItem = null; self.sourceItem = null;
if (self.mode === 'batchReplace') {
self.invalidItems = null;
} else if (self.mode === 'replaceInvalidItems') {
self.invalidItems = options.invalidItems;
}
self.targetItem = null; self.targetItem = null;
self.showState = true; self.showState = true;
@@ -277,10 +290,16 @@ export default {
}, },
confirm() { confirm() {
if (this.resolve) { if (this.resolve) {
this.resolve({ if (this.mode === 'batchReplace') {
sourceItem: this.sourceItem, this.resolve({
targetItem: this.targetItem targetItem: this.targetItem
}); });
} else if (this.mode === 'replaceInvalidItems') {
this.resolve({
sourceItem: this.sourceItem,
targetItem: this.targetItem
});
}
} }
this.showState = false; this.showState = false;
@@ -12,6 +12,27 @@
<v-icon :icon="icons.more" /> <v-icon :icon="icons.more" />
<v-menu activator="parent"> <v-menu activator="parent">
<v-list> <v-list>
<v-list-item :prepend-icon="icons.replace"
:disabled="selectedExpenseTransactionCount < 1"
:title="$t('Batch Replace Selected Expense Categories')"
@click="showBatchReplaceDialog('expenseCategory')"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="selectedIncomeTransactionCount < 1"
:title="$t('Batch Replace Selected Income Categories')"
@click="showBatchReplaceDialog('incomeCategory')"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="selectedTransferTransactionCount < 1"
:title="$t('Batch Replace Selected Transfer Categories')"
@click="showBatchReplaceDialog('transferCategory')"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="selectedImportTransactionCount < 1"
:title="$t('Batch Replace Selected Accounts')"
@click="showBatchReplaceDialog('account')"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="selectedTransferTransactionCount < 1"
:title="$t('Batch Replace Selected Destination Accounts')"
@click="showBatchReplaceDialog('destinationAccount')"></v-list-item>
<v-divider class="my-2"/>
<v-list-item :prepend-icon="icons.replace" <v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidExpenseCategoryNames < 1" :disabled="allInvalidExpenseCategoryNames < 1"
:title="$t('Replace Invalid Expense Categories')" :title="$t('Replace Invalid Expense Categories')"
@@ -241,7 +262,7 @@
<v-icon class="mr-1" :icon="icons.alert"/> <v-icon class="mr-1" :icon="icons.alert"/>
<span>{{ item.originalSourceAccountName }}</span> <span>{{ item.originalSourceAccountName }}</span>
</div> </div>
<v-icon class="mx-1" size="13" :icon="icons.arrowRight" v-if="item.type === allTransactionTypes.Transfer && item.sourceAccountId !== item.destinationAccountId"></v-icon> <v-icon class="mx-1" size="13" :icon="icons.arrowRight" v-if="item.type === allTransactionTypes.Transfer"></v-icon>
<span v-if="item.type === allTransactionTypes.Transfer && item.destinationAccountId && item.destinationAccountId !== '0' && allAccountsMap[item.destinationAccountId]">{{allAccountsMap[item.destinationAccountId].name }}</span> <span v-if="item.type === allTransactionTypes.Transfer && item.destinationAccountId && item.destinationAccountId !== '0' && allAccountsMap[item.destinationAccountId]">{{allAccountsMap[item.destinationAccountId].name }}</span>
<div class="text-error font-italic" v-else-if="item.type === allTransactionTypes.Transfer && (!item.destinationAccountId || item.destinationAccountId === '0' || !allAccountsMap[item.destinationAccountId])"> <div class="text-error font-italic" v-else-if="item.type === allTransactionTypes.Transfer && (!item.destinationAccountId || item.destinationAccountId === '0' || !allAccountsMap[item.destinationAccountId])">
<v-icon class="mr-1" :icon="icons.alert"/> <v-icon class="mr-1" :icon="icons.alert"/>
@@ -431,14 +452,14 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<replace-invalid-item-dialog ref="replaceInvalidItemDialog" /> <batch-replace-dialog ref="batchReplaceDialog" />
<confirm-dialog ref="confirmDialog"/> <confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" /> <snack-bar ref="snackbar" />
<input ref="fileInput" type="file" style="display: none" :accept="supportedImportFileExtensions" @change="setImportFile($event)" /> <input ref="fileInput" type="file" style="display: none" :accept="supportedImportFileExtensions" @change="setImportFile($event)" />
</template> </template>
<script> <script>
import ReplaceInvalidItemDialog from './ReplaceInvalidItemDialog.vue'; import BatchReplaceDialog from './BatchReplaceDialog.vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js'; import { useSettingsStore } from '@/stores/setting.js';
@@ -484,7 +505,7 @@ import {
export default { export default {
components: { components: {
ReplaceInvalidItemDialog BatchReplaceDialog
}, },
props: [ props: [
'persistent' 'persistent'
@@ -707,6 +728,39 @@ export default {
return count; return count;
}, },
selectedExpenseTransactionCount() {
let count = 0;
for (let i = 0; i < this.importTransactions.length; i++) {
if (this.importTransactions[i].selected && this.importTransactions[i].type === this.allTransactionTypes.Expense) {
count++;
}
}
return count;
},
selectedIncomeTransactionCount() {
let count = 0;
for (let i = 0; i < this.importTransactions.length; i++) {
if (this.importTransactions[i].selected && this.importTransactions[i].type === this.allTransactionTypes.Income) {
count++;
}
}
return count;
},
selectedTransferTransactionCount() {
let count = 0;
for (let i = 0; i < this.importTransactions.length; i++) {
if (this.importTransactions[i].selected && this.importTransactions[i].type === this.allTransactionTypes.Transfer) {
count++;
}
}
return count;
},
selectedInvalidTransactionCount() { selectedInvalidTransactionCount() {
let count = 0; let count = 0;
@@ -938,23 +992,17 @@ export default {
}, },
selectAllInThisPage() { selectAllInThisPage() {
for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) {
if (this.importTransactions[i] && this.importTransactions[i].valid) { this.importTransactions[i].selected = true;
this.importTransactions[i].selected = true;
}
} }
}, },
selectNoneInThisPage() { selectNoneInThisPage() {
for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) {
if (this.importTransactions[i]) { this.importTransactions[i].selected = false;
this.importTransactions[i].selected = false;
}
} }
}, },
selectInvertInThisPage() { selectInvertInThisPage() {
for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) { for (let i = Math.max(0, (this.currentPage - 1) * this.countPerPage); i < Math.min(this.importTransactions.length, this.currentPage * this.countPerPage); i++) {
if (this.importTransactions[i] && (this.importTransactions[i].valid || this.importTransactions[i].selected)) { this.importTransactions[i].selected = !this.importTransactions[i].selected;
this.importTransactions[i].selected = !this.importTransactions[i].selected;
}
} }
}, },
editTransaction(transaction) { editTransaction(transaction) {
@@ -967,10 +1015,71 @@ export default {
updateTransactionData(transaction) { updateTransactionData(transaction) {
transaction.valid = this.isTransactionValid(transaction); transaction.valid = this.isTransactionValid(transaction);
}, },
showBatchReplaceDialog(type) {
const self = this;
self.$refs.batchReplaceDialog.open({
mode: 'batchReplace',
type: type
}).then(result => {
if (!result || !result.targetItem) {
return;
}
let updatedCount = 0;
for (let i = 0; i < self.importTransactions.length; i++) {
const transaction = self.importTransactions[i];
if (!transaction.selected) {
continue;
}
let updated = false;
if (type === 'expenseCategory') {
if (transaction.type === self.allTransactionTypes.Expense) {
transaction.categoryId = result.targetItem;
updated = true;
}
} else if (type === 'incomeCategory') {
if (transaction.type === self.allTransactionTypes.Income) {
transaction.categoryId = result.targetItem;
updated = true;
}
} else if (type === 'transferCategory') {
if (transaction.type === self.allTransactionTypes.Transfer) {
transaction.categoryId = result.targetItem;
updated = true;
}
} else if (type === 'account') {
transaction.sourceAccountId = result.targetItem;
updated = true;
} else if (type === 'destinationAccount') {
if (transaction.type === self.allTransactionTypes.Transfer) {
transaction.destinationAccountId = result.targetItem;
updated = true;
}
}
if (updated) {
updatedCount++;
self.updateTransactionData(transaction);
}
}
if (updatedCount > 0) {
self.$refs.snackbar.showMessage('format.misc.youHaveUpdatedTransactions', {
count: updatedCount
});
}
});
},
showReplaceInvalidItemDialog(type, invalidItems) { showReplaceInvalidItemDialog(type, invalidItems) {
const self = this; const self = this;
self.$refs.replaceInvalidItemDialog.open({ self.$refs.batchReplaceDialog.open({
mode: 'replaceInvalidItems',
type: type, type: type,
invalidItems: invalidItems invalidItems: invalidItems
}).then(result => { }).then(result => {