mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-17 16:24:25 +08:00
load / save rules for batch replacing transaction categories / accounts / tags when import transactions
This commit is contained in:
@@ -158,6 +158,11 @@
|
||||
:title="tt('Replace Invalid Transaction Tags')"
|
||||
@click="showReplaceInvalidItemDialog('tag', allInvalidTransactionTagNames)"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiFindReplace"
|
||||
:disabled="!!editingTransaction"
|
||||
:title="tt('Batch Replace Categories / Accounts / Tags')"
|
||||
@click="showReplaceAllTypesDialog()"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiShapePlusOutline"
|
||||
:disabled="!!editingTransaction || !allInvalidExpenseCategoryNames || allInvalidExpenseCategoryNames.length < 1"
|
||||
:title="tt('Create Nonexistent Expense Categories')"
|
||||
@@ -862,6 +867,7 @@
|
||||
@dateRange:change="changeCustomDateFilter"
|
||||
@error="onShowDateRangeError" />
|
||||
<batch-replace-dialog ref="batchReplaceDialog" />
|
||||
<batch-replace-all-types-dialog ref="batchReplaceAllTypesDialog" />
|
||||
<batch-create-dialog ref="batchCreateDialog" />
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
@@ -874,6 +880,7 @@ import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
|
||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
import BatchReplaceDialog, { type BatchReplaceDialogDataType } from './dialogs/BatchReplaceDialog.vue';
|
||||
import BatchReplaceAllTypesDialog from './dialogs/BatchReplaceAllTypesDialog.vue';
|
||||
import BatchCreateDialog, { type BatchCreateDialogDataType } from './dialogs/BatchCreateDialog.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef, watch } from 'vue';
|
||||
@@ -962,6 +969,7 @@ import {
|
||||
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
type BatchReplaceDialogType = InstanceType<typeof BatchReplaceDialog>;
|
||||
type BatchReplaceAllTypesDialogType = InstanceType<typeof BatchReplaceAllTypesDialog>;
|
||||
type BatchCreateDialogType = InstanceType<typeof BatchCreateDialog>;
|
||||
|
||||
type ImportTransactionDialogStep = 'uploadFile' | 'defineColumn' | 'checkData' | 'finalResult';
|
||||
@@ -1007,6 +1015,7 @@ const statisticsStore = useStatisticsStore();
|
||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const batchReplaceDialog = useTemplateRef<BatchReplaceDialogType>('batchReplaceDialog');
|
||||
const batchReplaceAllTypesDialog = useTemplateRef<BatchReplaceAllTypesDialogType>('batchReplaceAllTypesDialog');
|
||||
const batchCreateDialog = useTemplateRef<BatchCreateDialogType>('batchCreateDialog');
|
||||
const fileInput = useTemplateRef<HTMLInputElement>('fileInput');
|
||||
|
||||
@@ -2479,6 +2488,78 @@ function showReplaceInvalidItemDialog(type: BatchReplaceDialogDataType, invalidI
|
||||
});
|
||||
}
|
||||
|
||||
function showReplaceAllTypesDialog(): void {
|
||||
if (editingTransaction.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
batchReplaceAllTypesDialog.value?.open({
|
||||
expenseCategoryNames: allInvalidExpenseCategoryNames.value,
|
||||
incomeCategoryNames: allInvalidIncomeCategoryNames.value,
|
||||
transferCategoryNames: allInvalidTransferCategoryNames.value,
|
||||
accountNames: allInvalidAccountNames.value,
|
||||
tagNames: allInvalidTransactionTagNames.value
|
||||
}).then(result => {
|
||||
if (!result || !result.rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedCount = 0;
|
||||
|
||||
if (importTransactions.value) {
|
||||
for (let i = 0; i < importTransactions.value.length; i++) {
|
||||
const transaction: ImportTransaction = importTransactions.value[i];
|
||||
let updated = false;
|
||||
|
||||
for (let j = 0; j < result.rules.length; j++) {
|
||||
const rule = result.rules[j];
|
||||
|
||||
if (!rule || !rule.dataType || !rule.sourceValue || !rule.targetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.dataType === 'expenseCategory' || rule.dataType === 'incomeCategory' || rule.dataType === 'transferCategory') {
|
||||
if (transaction.type !== TransactionType.ModifyBalance && transaction.originalCategoryName === rule.sourceValue) {
|
||||
transaction.categoryId = rule.targetId;
|
||||
updated = true;
|
||||
}
|
||||
} else if (rule.dataType === 'account') {
|
||||
if (transaction.originalSourceAccountName === rule.sourceValue) {
|
||||
transaction.sourceAccountId = rule.targetId;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (transaction.type === TransactionType.Transfer && transaction.originalDestinationAccountName === rule.sourceValue) {
|
||||
transaction.destinationAccountId = rule.targetId;
|
||||
updated = true;
|
||||
}
|
||||
} else if (rule.dataType === 'tag' && transaction.tagIds) {
|
||||
for (let k = 0; k < transaction.tagIds.length; k++) {
|
||||
const originalTagName = transaction.originalTagNames ? transaction.originalTagNames[k] : "";
|
||||
|
||||
if (originalTagName === rule.sourceValue) {
|
||||
transaction.tagIds[k] = rule.targetId;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
updatedCount++;
|
||||
updateTransactionData(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
|
||||
count: updatedCount
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showBatchCreateInvalidItemDialog(type: BatchCreateDialogDataType, invalidItems: NameValue[]): void {
|
||||
if (editingTransaction.value) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
<template>
|
||||
<v-dialog width="1000" :persistent="loading || !!rules.length || !!newRule.targetId" v-model="showState">
|
||||
<v-card class="pa-2 pa-sm-4 pa-md-4">
|
||||
<template #title>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<div class="d-flex w-100 align-center justify-center">
|
||||
<h4 class="text-h4">{{ tt('Batch Replace Categories / Accounts / Tags') }}</h4>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ml-2" :icon="true" :disabled="loading"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:icon="true" :disabled="loading">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent" max-height="500">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiFolderOpenOutline"
|
||||
:title="tt('Load Replace Rule File')"
|
||||
@click="loadReplaceRuleFile()"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiContentSaveOutline"
|
||||
:title="tt('Save Replace Rule File')"
|
||||
@click="saveReplaceRuleFile()"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<v-card-text class="my-md-4 w-100 d-flex justify-center">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-table fixed-header fixed-footer height="400" striped="even">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ tt('Type') }}</th>
|
||||
<th class="text-left">{{ tt('Source Value') }}</th>
|
||||
<th class="text-left">{{ tt('Target Value') }}</th>
|
||||
<th class="text-right">{{ tt('Operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(rule, index) in rules" :key="index">
|
||||
<td class="text-left">{{ getRuleTypeDisplayName(rule) }}</td>
|
||||
<td class="text-left">{{ rule.sourceValue || tt('(Empty)') }}</td>
|
||||
<td class="text-left">{{ getRuleTargetValueDisplayName(rule) }}</td>
|
||||
<td class="text-right">
|
||||
<v-btn density="comfortable" variant="tonal" color="error"
|
||||
:disabled="loading" @click="removeRule(index)">{{ tt('Delete') }}</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="background-color: rgb(var(--v-theme-surface))">
|
||||
<td>
|
||||
<v-select class="w-100" density="compact" variant="outlined"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
:disabled="loading"
|
||||
:items="[
|
||||
{
|
||||
value: 'expenseCategory',
|
||||
name: tt('Expense Category')
|
||||
},
|
||||
{
|
||||
value: 'incomeCategory',
|
||||
name: tt('Income Category')
|
||||
},
|
||||
{
|
||||
value: 'transferCategory',
|
||||
name: tt('Transfer Category')
|
||||
},
|
||||
{
|
||||
value: 'account',
|
||||
name: tt('Account')
|
||||
},
|
||||
{
|
||||
value: 'tag',
|
||||
name: tt('Transaction Tag')
|
||||
}
|
||||
]"
|
||||
v-model="newRule.dataType"
|
||||
@update:model-value="newRule.sourceValue = ''; newRule.targetId = ''"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-autocomplete class="w-100" density="compact" variant="outlined"
|
||||
item-title="name" item-value="value" persistent-placeholder
|
||||
:disabled="loading" :items="sourceItems"
|
||||
:no-data-text="noSourceItemText"
|
||||
v-model="newRule.sourceValue">
|
||||
</v-autocomplete>
|
||||
</td>
|
||||
<td>
|
||||
<two-column-select density="compact" variant="outlined"
|
||||
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="loading || !hasAvailableExpenseCategories"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
||||
:show-selection-primary-text="true"
|
||||
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Expense])"
|
||||
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(newRule.targetId, allCategories[CategoryType.Expense])"
|
||||
:items="allCategories[CategoryType.Expense]"
|
||||
v-model="newRule.targetId"
|
||||
v-if="newRule.dataType === 'expenseCategory'">
|
||||
</two-column-select>
|
||||
<two-column-select density="compact" variant="outlined"
|
||||
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="loading || !hasAvailableIncomeCategories"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
||||
:show-selection-primary-text="true"
|
||||
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Income])"
|
||||
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(newRule.targetId, allCategories[CategoryType.Income])"
|
||||
:items="allCategories[CategoryType.Income]"
|
||||
v-model="newRule.targetId"
|
||||
v-if="newRule.dataType === 'incomeCategory'">
|
||||
</two-column-select>
|
||||
<two-column-select density="compact" variant="outlined"
|
||||
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="loading || !hasAvailableTransferCategories"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
||||
:show-selection-primary-text="true"
|
||||
:custom-selection-primary-text="getTransactionPrimaryCategoryName(newRule.targetId, allCategories[CategoryType.Transfer])"
|
||||
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(newRule.targetId, allCategories[CategoryType.Transfer])"
|
||||
:items="allCategories[CategoryType.Transfer]"
|
||||
v-model="newRule.targetId"
|
||||
v-if="newRule.dataType === 'transferCategory'">
|
||||
</two-column-select>
|
||||
<two-column-select density="compact" variant="outlined"
|
||||
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="loading || !allVisibleAccounts.length"
|
||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||
:custom-selection-primary-text="getAccountDisplayName(newRule.targetId)"
|
||||
:items="allVisibleCategorizedAccounts"
|
||||
v-model="newRule.targetId"
|
||||
v-if="newRule.dataType === 'account'">
|
||||
</two-column-select>
|
||||
<v-autocomplete density="compact" variant="outlined"
|
||||
item-title="name" item-value="id"
|
||||
persistent-placeholder chips
|
||||
:disabled="loading" :items="allTags"
|
||||
:no-data-text="tt('No available tag')"
|
||||
v-model="newRule.targetId"
|
||||
v-if="newRule.dataType == 'tag'">
|
||||
<template #chip="{ props, item }">
|
||||
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props" v-if="newRule.targetId"/>
|
||||
</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="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<v-btn density="comfortable" variant="tonal" color="primary"
|
||||
:disabled="loading || !newRule.dataType || !newRule.targetId"
|
||||
@click="addNewRule()">{{ tt('Add Rule') }}</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</v-table>
|
||||
</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="loading" @click="confirm">{{ tt('OK') }}</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" :disabled="loading" @click="cancel">{{ tt('Cancel') }}</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import type { NameValue } from '@/core/base.ts';
|
||||
import { CategoryType } from '@/core/category.ts';
|
||||
import { ImportTransactionReplaceRule, ImportTransactionReplaceRules } from '@/core/import_transaction.ts';
|
||||
import { KnownFileType } from '@/core/file.ts';
|
||||
|
||||
import { Account, type CategorizedAccountWithDisplayBalance } from '@/models/account.ts';
|
||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
getTransactionPrimaryCategoryName,
|
||||
getTransactionSecondaryCategoryName
|
||||
} from '@/lib/category.ts';
|
||||
|
||||
import logger from '@/lib/logger.ts';
|
||||
|
||||
import {
|
||||
openTextFileContent,
|
||||
startDownloadFile
|
||||
} from '@/lib/ui/common.ts';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiDotsVertical,
|
||||
mdiFolderOpenOutline,
|
||||
mdiContentSaveOutline, mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
interface BatchReplaceAllTypesDialogResponse {
|
||||
rules: ImportTransactionReplaceRule[]
|
||||
}
|
||||
|
||||
const { tt, getCategorizedAccountsWithDisplayBalance } = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const showState = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
const rules = ref<ImportTransactionReplaceRule[]>([]);
|
||||
const newRule = ref<ImportTransactionReplaceRule>(ImportTransactionReplaceRule.of('expenseCategory', '', ''));
|
||||
|
||||
const sourceExpenseCategoryNames = ref<NameValue[]>([]);
|
||||
const sourceIncomeCategoryNames = ref<NameValue[]>([]);
|
||||
const sourceTransferCategoryNames = ref<NameValue[]>([]);
|
||||
const sourceAccountNames = ref<NameValue[]>([]);
|
||||
const sourceTagNames = ref<NameValue[]>([]);
|
||||
|
||||
let resolveFunc: ((response: BatchReplaceAllTypesDialogResponse) => void) | null = null;
|
||||
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||
|
||||
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
|
||||
const allAccounts = computed<Account[]>(() => accountsStore.allPlainAccounts);
|
||||
const allVisibleAccounts = computed<Account[]>(() => accountsStore.allVisiblePlainAccounts);
|
||||
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value));
|
||||
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
|
||||
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
|
||||
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
|
||||
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
|
||||
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
|
||||
|
||||
const sourceItems = computed<NameValue[]>(() => {
|
||||
switch (newRule.value.dataType) {
|
||||
case 'expenseCategory':
|
||||
return sourceExpenseCategoryNames.value;
|
||||
case 'incomeCategory':
|
||||
return sourceIncomeCategoryNames.value;
|
||||
case 'transferCategory':
|
||||
return sourceTransferCategoryNames.value;
|
||||
case 'account':
|
||||
return sourceAccountNames.value;
|
||||
case 'tag':
|
||||
return sourceTagNames.value;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const noSourceItemText = computed<string>(() => {
|
||||
switch (newRule.value.dataType) {
|
||||
case 'expenseCategory':
|
||||
return tt('No available category');
|
||||
case 'incomeCategory':
|
||||
return tt('No available category');
|
||||
case 'transferCategory':
|
||||
return tt('No available category');
|
||||
case 'account':
|
||||
return tt('No available account');
|
||||
case 'tag':
|
||||
return tt('No available tag');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
function getRuleTypeDisplayName(rule: ImportTransactionReplaceRule): string {
|
||||
switch (rule.dataType) {
|
||||
case 'expenseCategory':
|
||||
return tt('Expense Category');
|
||||
case 'incomeCategory':
|
||||
return tt('Income Category');
|
||||
case 'transferCategory':
|
||||
return tt('Transfer Category');
|
||||
case 'account':
|
||||
return tt('Account');
|
||||
case 'tag':
|
||||
return tt('Transaction Tag');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getRuleTargetValueDisplayName(rule: ImportTransactionReplaceRule): string {
|
||||
switch (rule.dataType) {
|
||||
case 'expenseCategory':
|
||||
return getTransactionSecondaryCategoryName(rule.targetId, allCategories.value[CategoryType.Expense]) || '';
|
||||
case 'incomeCategory':
|
||||
return getTransactionSecondaryCategoryName(rule.targetId, allCategories.value[CategoryType.Income]) || '';
|
||||
case 'transferCategory':
|
||||
return getTransactionSecondaryCategoryName(rule.targetId, allCategories.value[CategoryType.Transfer]) || '';
|
||||
case 'account':
|
||||
return getAccountDisplayName(rule.targetId);
|
||||
case 'tag':
|
||||
for (const tag of allTags.value) {
|
||||
if (tag.id === rule.targetId) {
|
||||
return tag.name;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getAccountDisplayName(accountId?: string): string {
|
||||
if (accountId) {
|
||||
return Account.findAccountNameById(allAccounts.value, accountId) || '';
|
||||
} else {
|
||||
return tt('None');
|
||||
}
|
||||
}
|
||||
|
||||
function open(options: { expenseCategoryNames: NameValue[], incomeCategoryNames: NameValue[], transferCategoryNames: NameValue[], accountNames: NameValue[], tagNames: NameValue[] }): Promise<BatchReplaceAllTypesDialogResponse> {
|
||||
rules.value = [];
|
||||
newRule.value = ImportTransactionReplaceRule.of('expenseCategory', '', '');
|
||||
sourceExpenseCategoryNames.value = options.expenseCategoryNames;
|
||||
sourceIncomeCategoryNames.value = options.incomeCategoryNames;
|
||||
sourceTransferCategoryNames.value = options.transferCategoryNames;
|
||||
sourceAccountNames.value = options.accountNames;
|
||||
sourceTagNames.value = options.tagNames;
|
||||
showState.value = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveFunc = resolve;
|
||||
rejectFunc = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
loading.value = true;
|
||||
|
||||
Promise.all([
|
||||
accountsStore.loadAllAccounts({ force: true }),
|
||||
transactionCategoriesStore.loadAllCategories({ force: true }),
|
||||
transactionTagsStore.loadAllTags({ force: true })
|
||||
]).then(() => {
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadReplaceRuleFile(): void {
|
||||
openTextFileContent({
|
||||
allowedExtensions: KnownFileType.JSON.contentType
|
||||
}).then(content => {
|
||||
const result = ImportTransactionReplaceRules.parseFromJson(content);
|
||||
|
||||
if (result) {
|
||||
rules.value = result.getRules();
|
||||
} else {
|
||||
logger.error('Failed to parse replace rule file');
|
||||
snackbar.value?.showError('Replace rule file is invalid');
|
||||
}
|
||||
}).catch(error => {
|
||||
logger.error('Failed to open replace rule file', error);
|
||||
snackbar.value?.showError('Replace rule file is invalid');
|
||||
});
|
||||
}
|
||||
|
||||
function saveReplaceRuleFile(): void {
|
||||
const fileName = KnownFileType.JSON.formatFileName(tt('dataExport.defaultImportReplaceRuleFileName'));
|
||||
startDownloadFile(fileName, KnownFileType.JSON.createBlob(ImportTransactionReplaceRules.of(rules.value).toJson()));
|
||||
}
|
||||
|
||||
function removeRule(index: number): void {
|
||||
rules.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function addNewRule(): void {
|
||||
if (!newRule.value.dataType || !newRule.value.targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
rules.value.push(newRule.value);
|
||||
newRule.value = ImportTransactionReplaceRule.of('expenseCategory', '', '');
|
||||
}
|
||||
|
||||
function confirm(): void {
|
||||
resolveFunc?.({
|
||||
rules: rules.value
|
||||
});
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
rejectFunc?.();
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user