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

This commit is contained in:
MaysWind
2024-09-23 00:19:03 +08:00
parent 21ea36a4f7
commit 29781bbac4
5 changed files with 566 additions and 6 deletions
+2 -2
View File
@@ -37,9 +37,9 @@ export default {
}
},
methods: {
showMessage(message) {
showMessage(message, options) {
this.showState = true;
this.messageContent = this.$t(message);
this.messageContent = this.$t(message, options);
},
showError(error) {
this.showState = true;
+14
View File
@@ -81,6 +81,7 @@
"youHaveAccounts": "You have recorded {count} accounts",
"clickToSelectedFile": "Click to select import file ({extensions})",
"selectedCount": "Selected {count} of {totalCount}",
"youHaveUpdatedTransactions": "You have updated {count} transactions",
"confirmImportTransactions": "Are you sure you want to import {count} transactions?",
"importTransactionResult": "You have imported {count} transactions successfully.",
"accountActivationAndResendValidationEmailTip": "Account activation link has been sent to your email address: {email}, If you don't receive the mail, please fill password again and click the button below to resend the validation mail.",
@@ -1508,6 +1509,19 @@
"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",
"Replace Invalid Expense Categories": "Replace Invalid Expense Categories",
"Replace Invalid Income Categories": "Replace Invalid Income Categories",
"Replace Invalid Transfer Categories": "Replace Invalid Transfer Categories",
"Replace Invalid Accounts": "Replace Invalid Accounts",
"Replace Invalid Transaction Tags": "Replace Invalid Transaction Tags",
"Invalid Category": "Invalid Category",
"Target Category": "Target Category",
"Invalid Account": "Invalid Account",
"Target Account": "Target Account",
"Invalid Tag": "Invalid Tag",
"Target Tag": "Target Tag",
"(Empty)": "(Empty)",
"Tags": "Tags",
"Your transaction description (optional)": "Your transaction description (optional)",
"Transaction category cannot be blank": "Transaction category cannot be blank",
+14
View File
@@ -81,6 +81,7 @@
"youHaveAccounts": "您已经记录了 {count} 个账户",
"clickToSelectedFile": "点击选择导入文件 ({extensions})",
"selectedCount": "已选择 {count} / {totalCount}",
"youHaveUpdatedTransactions": "您已经更新 {count} 个交易",
"confirmImportTransactions": "您确定要导入 {count} 个交易?",
"importTransactionResult": "您已经成功导入 {count} 个交易。",
"accountActivationAndResendValidationEmailTip": "账号激活链接已经发送到您的邮箱地址:{email},如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件。",
@@ -1508,6 +1509,19 @@
"No data to import": "没有可以导入的数据",
"Cannot import invalid transactions": "不能导入无效的交易",
"Unable to parse import file": "无法解析导入的文件",
"Batch Replace": "批量替换",
"Replace Invalid Expense Categories": "替换无效的支出分类",
"Replace Invalid Income Categories": "替换无效的收入分类",
"Replace Invalid Transfer Categories": "替换无效的转账分类",
"Replace Invalid Accounts": "替换无效的账户",
"Replace Invalid Transaction Tags": "替换无效的交易标签",
"Invalid Category": "无效分类",
"Target Category": "目标分类",
"Invalid Account": "无效账户",
"Target Account": "目标账户",
"Invalid Tag": "无效标签",
"Target Tag": "目标标签",
"(Empty)": "(空白)",
"Tags": "标签",
"Your transaction description (optional)": "你的交易描述 (可选)",
"Transaction category cannot be blank": "交易分类不能为空",
@@ -7,6 +7,34 @@
<h4 class="text-h4">{{ $t('Import Transactions') }}</h4>
<v-progress-circular indeterminate size="22" class="ml-2" v-if="loading"></v-progress-circular>
</div>
<v-btn density="comfortable" color="default" variant="text" class="ml-2" :icon="true"
v-if="currentStep === 'checkData'">
<v-icon :icon="icons.more" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidExpenseCategoryNames < 1"
:title="$t('Replace Invalid Expense Categories')"
@click="showReplaceInvalidItemDialog('expenseCategory', allInvalidExpenseCategoryNames)"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidIncomeCategoryNames < 1"
:title="$t('Replace Invalid Income Categories')"
@click="showReplaceInvalidItemDialog('incomeCategory', allInvalidIncomeCategoryNames)"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidTransferCategoryNames < 1"
:title="$t('Replace Invalid Transfer Categories')"
@click="showReplaceInvalidItemDialog('transferCategory', allInvalidTransferCategoryNames)"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidAccountNames < 1"
:title="$t('Replace Invalid Accounts')"
@click="showReplaceInvalidItemDialog('account', allInvalidAccountNames)"></v-list-item>
<v-list-item :prepend-icon="icons.replace"
:disabled="allInvalidTransactionTagNames < 1"
:title="$t('Replace Invalid Transaction Tags')"
@click="showReplaceInvalidItemDialog('tag', allInvalidTransactionTagNames)"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
@@ -131,7 +159,7 @@
<v-chip label color="primary" variant="outlined" size="x-small" v-else-if="value === allTransactionTypes.Transfer">{{ $t('Transfer') }}</v-chip>
<v-chip label color="default" variant="outlined" size="x-small" v-else>{{ $t('Unknown') }}</v-chip>
</template>
<template #item.categoryId="{ item }">
<template #item.originalCategoryName="{ item }">
<div class="d-flex align-center" v-if="editingTransaction !== item || item.type === allTransactionTypes.ModifyBalance">
<span v-if="item.type === allTransactionTypes.ModifyBalance">-</span>
<ItemIcon size="24px" icon-type="category"
@@ -206,7 +234,7 @@
<v-icon class="mx-1" size="13" :icon="icons.arrowRight" v-if="item.type === allTransactionTypes.Transfer && item.sourceAccountId !== item.destinationAccountId"></v-icon>
<span v-if="item.type === allTransactionTypes.Transfer && item.sourceAccountId !== item.destinationAccountId">{{ getTransactionDisplayDestinationAmount(item) }}</span>
</template>
<template #item.sourceAccountId="{ item }">
<template #item.originalSourceAccountName="{ item }">
<div class="d-flex align-center" v-if="editingTransaction !== item">
<span v-if="item.sourceAccountId && item.sourceAccountId !== '0' && allAccountsMap[item.sourceAccountId]">{{ allAccountsMap[item.sourceAccountId].name }}</span>
<div class="text-error font-italic" v-else>
@@ -403,12 +431,15 @@
</v-card>
</v-dialog>
<replace-invalid-item-dialog ref="replaceInvalidItemDialog" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
<input ref="fileInput" type="file" style="display: none" :accept="supportedImportFileExtensions" @change="setImportFile($event)" />
</template>
<script>
import ReplaceInvalidItemDialog from './ReplaceInvalidItemDialog.vue';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
@@ -438,6 +469,8 @@ import {
} from '@/lib/category.js';
import {
mdiDotsVertical,
mdiFindReplace,
mdiClose,
mdiArrowRight,
mdiCheck,
@@ -450,6 +483,9 @@ import {
} from '@mdi/js';
export default {
components: {
ReplaceInvalidItemDialog
},
props: [
'persistent'
],
@@ -473,6 +509,8 @@ export default {
resolve: null,
reject: null,
icons: {
more: mdiDotsVertical,
replace: mdiFindReplace,
previous: mdiClose,
next: mdiArrowRight,
complete: mdiCheck,
@@ -598,9 +636,9 @@ export default {
{ value: 'valid', sortable: true, nowrap: true, width: 35 },
{ value: 'time', title: this.$t('Transaction Time'), sortable: true, nowrap: true, maxWidth: 280 },
{ value: 'type', title: this.$t('Type'), sortable: true, nowrap: true, maxWidth: 140 },
{ value: 'categoryId', title: this.$t('Category'), sortable: true, nowrap: true },
{ value: 'originalCategoryName', title: this.$t('Category'), sortable: true, nowrap: true },
{ value: 'sourceAmount', title: this.$t('Amount'), sortable: true, nowrap: true },
{ value: 'sourceAccountId', title: this.$t('Account'), sortable: true, nowrap: true },
{ value: 'originalSourceAccountName', title: this.$t('Account'), sortable: true, nowrap: true },
{ value: 'geoLocation', title: this.$t('Geographic Location'), sortable: true, nowrap: true },
{ value: 'tagIds', title: this.$t('Tags'), sortable: true, nowrap: true },
{ value: 'comment', title: this.$t('Description'), sortable: true, nowrap: true },
@@ -689,6 +727,21 @@ export default {
get: function () {
return this.selectedImportTransactionCount === this.importTransactions.length;
}
},
allInvalidExpenseCategoryNames() {
return this.getCurrentInvalidCategoryNames(this.allTransactionTypes.Expense);
},
allInvalidIncomeCategoryNames() {
return this.getCurrentInvalidCategoryNames(this.allTransactionTypes.Income);
},
allInvalidTransferCategoryNames() {
return this.getCurrentInvalidCategoryNames(this.allTransactionTypes.Transfer);
},
allInvalidAccountNames() {
return this.getCurrentInvalidAccountNames();
},
allInvalidTransactionTagNames() {
return this.getCurrentInvalidTagNames();
}
},
watch: {
@@ -914,6 +967,175 @@ export default {
updateTransactionData(transaction) {
transaction.valid = this.isTransactionValid(transaction);
},
showReplaceInvalidItemDialog(type, invalidItems) {
const self = this;
self.$refs.replaceInvalidItemDialog.open({
type: type,
invalidItems: invalidItems
}).then(result => {
if (!result || (!result.sourceItem && result.sourceItem !== '') || !result.targetItem) {
return;
}
let updatedCount = 0;
for (let i = 0; i < self.importTransactions.length; i++) {
const transaction = self.importTransactions[i];
if (transaction.valid) {
continue;
}
let updated = false;
if (type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory') {
const categoryId = transaction.categoryId;
const originalCategoryName = transaction.originalCategoryName;
if (transaction.type !== self.allTransactionTypes.ModifyBalance && originalCategoryName === result.sourceItem && (!categoryId || categoryId === '0' || !self.allCategoriesMap[categoryId])) {
if (type === 'expenseCategory' && transaction.type === self.allTransactionTypes.Expense) {
transaction.categoryId = result.targetItem;
updated = true;
} else if (type === 'incomeCategory' && transaction.type === self.allTransactionTypes.Income) {
transaction.categoryId = result.targetItem;
updated = true;
} else if (type === 'transferCategory' && transaction.type === self.allTransactionTypes.Transfer) {
transaction.categoryId = result.targetItem;
updated = true;
}
}
} else if (type === 'account') {
const sourceAccountId = transaction.sourceAccountId;
const originalSourceAccountName = transaction.originalSourceAccountName;
const destinationAccountId = transaction.destinationAccountId;
const originalDestinationAccountName = transaction.originalDestinationAccountName;
if (originalSourceAccountName === result.sourceItem && (!sourceAccountId || sourceAccountId === '0' || !self.allAccountsMap[sourceAccountId])) {
transaction.sourceAccountId = result.targetItem;
updated = true;
}
if (transaction.type === self.allTransactionTypes.Transfer && originalDestinationAccountName === result.sourceItem && (!destinationAccountId || destinationAccountId === '0' || !self.allAccountsMap[destinationAccountId])) {
transaction.destinationAccountId = result.targetItem;
updated = true;
}
} else if (type === 'tag') {
for (let j = 0; j < transaction.tagIds.length; j++) {
const tagId = transaction.tagIds[j];
const originalTagName = transaction.originalTagNames[j];
if (originalTagName === result.sourceItem && (!tagId || tagId === '0' || !self.allTagsMap[tagId])) {
transaction.tagIds[j] = result.targetItem;
updated = true;
}
}
}
if (updated) {
updatedCount++;
self.updateTransactionData(transaction);
}
}
if (updatedCount > 0) {
self.$refs.snackbar.showMessage('format.misc.youHaveUpdatedTransactions', {
count: updatedCount
});
}
});
},
getCurrentInvalidCategoryNames(transactionType) {
const invalidCategoryNames = {};
const invalidCategories = [];
for (let i = 0; i < this.importTransactions.length; i++) {
const transaction = this.importTransactions[i];
const categoryId = transaction.categoryId;
if (transaction.type === transactionType && (!categoryId || categoryId === '0' || !this.allCategoriesMap[categoryId])) {
invalidCategoryNames[transaction.originalCategoryName] = true;
}
}
for (let name in invalidCategoryNames) {
if (!Object.prototype.hasOwnProperty.call(invalidCategoryNames, name)) {
continue;
}
invalidCategories.push({
name: name || this.$t('(Empty)'),
value: name
});
}
return invalidCategories;
},
getCurrentInvalidAccountNames() {
const invalidAccountNames = {};
const invalidAccounts = [];
for (let i = 0; i < this.importTransactions.length; i++) {
const transaction = this.importTransactions[i];
const sourceAccountId = transaction.sourceAccountId;
const destinationAccountId = transaction.destinationAccountId;
if (!sourceAccountId || sourceAccountId === '0' || !this.allAccountsMap[sourceAccountId]) {
invalidAccountNames[transaction.originalSourceAccountName] = true;
}
if (transaction.type === this.allTransactionTypes.Transfer && (!destinationAccountId || destinationAccountId === '0' || !this.allAccountsMap[destinationAccountId])) {
invalidAccountNames[transaction.originalDestinationAccountName] = true;
}
}
for (let name in invalidAccountNames) {
if (!Object.prototype.hasOwnProperty.call(invalidAccountNames, name)) {
continue;
}
invalidAccounts.push({
name: name || this.$t('(Empty)'),
value: name
});
}
return invalidAccounts;
},
getCurrentInvalidTagNames() {
const invalidTagNames = {};
const invalidTags = [];
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]) {
invalidTagNames[originalTagName] = true;
}
}
}
for (let name in invalidTagNames) {
if (!Object.prototype.hasOwnProperty.call(invalidTagNames, name)) {
continue;
}
invalidTags.push({
name: name || this.$t('(Empty)'),
value: name
});
}
return invalidTags;
},
isTransactionValid(transaction) {
if (!transaction) {
return false;
@@ -0,0 +1,310 @@
<template>
<v-dialog width="430" :persistent="!!persistent" v-model="showState">
<v-card class="pa-2 pa-sm-4 pa-md-4">
<template #title>
<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="type === 'incomeCategory'">{{ $t('Replace Invalid Income Categories') }}</h4>
<h4 class="text-h4" v-if="type === 'transferCategory'">{{ $t('Replace Invalid Transfer Categories') }}</h4>
<h4 class="text-h4" v-if="type === 'account'">{{ $t('Replace Invalid Accounts') }}</h4>
<h4 class="text-h4" v-if="type === 'tag'">{{ $t('Replace Invalid Transaction Tags') }}</h4>
</div>
</template>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory'">
<v-row>
<v-col cols="12">
<v-autocomplete
item-title="name"
item-value="value"
persistent-placeholder
:label="$t('Invalid Category')"
:placeholder="$t('Invalid Category')"
:items="invalidItems"
:no-data-text="$t('No available category')"
v-model="sourceItem">
</v-autocomplete>
</v-col>
<v-col cols="12">
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!hasAvailableExpenseCategories"
:show-selection-primary-text="true"
:custom-selection-primary-text="getPrimaryCategoryName(targetItem, allCategories[allCategoryTypes.Expense])"
:custom-selection-secondary-text="getSecondaryCategoryName(targetItem, allCategories[allCategoryTypes.Expense])"
:label="$t('Target Category')"
:placeholder="$t('Target Category')"
:items="allCategories[allCategoryTypes.Expense]"
v-model="targetItem"
v-if="type === 'expenseCategory'">
</two-column-select>
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!hasAvailableIncomeCategories"
:show-selection-primary-text="true"
:custom-selection-primary-text="getPrimaryCategoryName(targetItem, allCategories[allCategoryTypes.Income])"
:custom-selection-secondary-text="getSecondaryCategoryName(targetItem, allCategories[allCategoryTypes.Income])"
:label="$t('Target Category')"
:placeholder="$t('Target Category')"
:items="allCategories[allCategoryTypes.Income]"
v-model="targetItem"
v-if="type === 'incomeCategory'">
</two-column-select>
<two-column-select primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-hidden-field="hidden" primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:disabled="!hasAvailableTransferCategories"
:show-selection-primary-text="true"
:custom-selection-primary-text="getPrimaryCategoryName(targetItem, allCategories[allCategoryTypes.Transfer])"
:custom-selection-secondary-text="getSecondaryCategoryName(targetItem, allCategories[allCategoryTypes.Transfer])"
:label="$t('Target Category')"
:placeholder="$t('Target Category')"
:items="allCategories[allCategoryTypes.Transfer]"
v-model="targetItem"
v-if="type === 'transferCategory'">
</two-column-select>
</v-col>
</v-row>
</v-card-text>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'account'">
<v-row>
<v-col cols="12">
<v-autocomplete
item-title="name"
item-value="value"
persistent-placeholder
:label="$t('Invalid Account')"
:placeholder="$t('Invalid Account')"
:items="invalidItems"
:no-data-text="$t('No available account')"
v-model="sourceItem">
</v-autocomplete>
</v-col>
<v-col cols="12">
<two-column-select primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:disabled="!allVisibleAccounts.length"
:custom-selection-primary-text="getAccountDisplayName(targetItem)"
:label="$t('Target Account')"
:placeholder="$t('Target Account')"
:items="allVisibleCategorizedAccounts"
v-model="targetItem">
</two-column-select>
</v-col>
</v-row>
</v-card-text>
<v-card-text class="my-md-4 w-100 d-flex justify-center" v-if="type === 'tag'">
<v-row>
<v-col cols="12">
<v-autocomplete
item-title="name"
item-value="value"
persistent-placeholder
:label="$t('Invalid Tag')"
:placeholder="$t('Invalid Tag')"
:items="invalidItems"
:no-data-text="$t('No available tag')"
v-model="sourceItem">
</v-autocomplete>
</v-col>
<v-col cols="12">
<v-autocomplete
item-title="name"
item-value="id"
persistent-placeholder
chips
:label="$t('Target Tag')"
:placeholder="$t('Target Tag')"
:items="allTags"
:no-data-text="$t('No available tag')"
v-model="targetItem"
>
<template #chip="{ props, item }">
<v-chip :prepend-icon="icons.tag" :text="item.title" v-bind="props"/>
</template>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">
<v-icon size="20" start :icon="icons.tag"/>
<span>{{ item.title }}</span>
</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
</v-row>
</v-card-text>
<v-card-text class="overflow-y-visible">
<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 color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import { useAccountsStore } from '@/stores/account.js';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js';
import { useTransactionTagsStore } from '@/stores/transactionTag.js';
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
import categoryConstants from '@/consts/category.js';
import {
getNameByKeyValue
} from '@/lib/common.js';
import {
getTransactionPrimaryCategoryName,
getTransactionSecondaryCategoryName,
getFirstAvailableCategoryId
} from '@/lib/category.js';
import {
mdiPound
} from '@mdi/js';
export default {
props: [
'persistent'
],
expose: [
'open'
],
data() {
return {
showState: false,
type: '',
invalidItems: [],
sourceItem: null,
targetItem: null,
icons: {
tag: mdiPound
}
}
},
computed: {
...mapStores(useSettingsStore, useUserStore, useAccountsStore, useTransactionCategoriesStore, useTransactionTagsStore, useExchangeRatesStore),
defaultCurrency() {
return this.userStore.currentUserDefaultCurrency;
},
allCategoryTypes() {
return categoryConstants.allCategoryTypes;
},
allAccounts() {
return this.accountsStore.allPlainAccounts;
},
allVisibleCategorizedAccounts() {
return this.$locale.getCategorizedAccountsWithDisplayBalance(this.allVisibleAccounts, this.showAccountBalance, this.defaultCurrency, this.settingsStore, this.userStore, this.exchangeRatesStore);
},
allVisibleAccounts() {
return this.accountsStore.allVisiblePlainAccounts;
},
allCategories() {
return this.transactionCategoriesStore.allTransactionCategories;
},
allTags() {
return this.transactionTagsStore.allTransactionTags;
},
allTagsMap() {
return this.transactionTagsStore.allTransactionTagsMap;
},
hasAvailableExpenseCategories() {
if (!this.allCategories || !this.allCategories[this.allCategoryTypes.Expense] || !this.allCategories[this.allCategoryTypes.Expense].length) {
return false;
}
const firstAvailableCategoryId = getFirstAvailableCategoryId(this.allCategories[this.allCategoryTypes.Expense]);
return firstAvailableCategoryId !== '';
},
hasAvailableIncomeCategories() {
if (!this.allCategories || !this.allCategories[this.allCategoryTypes.Income] || !this.allCategories[this.allCategoryTypes.Income].length) {
return false;
}
const firstAvailableCategoryId = getFirstAvailableCategoryId(this.allCategories[this.allCategoryTypes.Income]);
return firstAvailableCategoryId !== '';
},
hasAvailableTransferCategories() {
if (!this.allCategories || !this.allCategories[this.allCategoryTypes.Transfer] || !this.allCategories[this.allCategoryTypes.Transfer].length) {
return false;
}
const firstAvailableCategoryId = getFirstAvailableCategoryId(this.allCategories[this.allCategoryTypes.Transfer]);
return firstAvailableCategoryId !== '';
},
showAccountBalance() {
return this.settingsStore.appSettings.showAccountBalance;
},
},
methods: {
open(options) {
const self = this;
self.type = options.type;
self.invalidItems = options.invalidItems;
self.sourceItem = null;
self.targetItem = null;
self.showState = true;
return new Promise((resolve, reject) => {
self.resolve = resolve;
self.reject = reject;
});
},
confirm() {
if (this.resolve) {
this.resolve({
sourceItem: this.sourceItem,
targetItem: this.targetItem
});
}
this.showState = false;
},
cancel() {
if (this.reject) {
this.reject();
}
this.showState = false;
},
getPrimaryCategoryName(categoryId, allCategories) {
return getTransactionPrimaryCategoryName(categoryId, allCategories);
},
getSecondaryCategoryName(categoryId, allCategories) {
return getTransactionSecondaryCategoryName(categoryId, allCategories);
},
getAccountDisplayName(accountId) {
if (accountId) {
return getNameByKeyValue(this.allAccounts, accountId, 'id', 'name');
} else {
return this.$t('None');
}
}
}
}
</script>