1860 lines
81 KiB
Vue
1860 lines
81 KiB
Vue
<template>
|
|
<v-data-table
|
|
fixed-header
|
|
fixed-footer
|
|
show-select
|
|
multi-sort
|
|
density="compact"
|
|
item-value="index"
|
|
:class="{ 'import-transaction-table': true, 'disabled': !!disabled }"
|
|
:height="importTransactionsTableHeight"
|
|
:headers="importTransactionHeaders"
|
|
:items="importTransactions"
|
|
:search="JSON.stringify(filters)"
|
|
:custom-filter="importTransactionsFilter"
|
|
:no-data-text="tt('No data to import')"
|
|
v-model:items-per-page="countPerPage"
|
|
v-model:page="currentPage"
|
|
>
|
|
<template #header.data-table-select>
|
|
<v-checkbox readonly class="always-cursor-pointer"
|
|
density="compact" width="28"
|
|
:disabled="!!disabled"
|
|
:indeterminate="anyButNotAllTransactionSelected"
|
|
v-model="allTransactionSelected"
|
|
>
|
|
<v-menu activator="parent" location="bottom">
|
|
<v-list>
|
|
<v-list-item :prepend-icon="mdiSelectAll"
|
|
:title="tt('Select All Valid Items')"
|
|
:disabled="!!disabled"
|
|
@click="selectAllValid"></v-list-item>
|
|
<v-list-item :prepend-icon="mdiSelectAll"
|
|
:title="tt('Select All Invalid Items')"
|
|
:disabled="!!disabled"
|
|
@click="selectAllInvalid"></v-list-item>
|
|
<v-divider class="my-2"/>
|
|
<v-list-item :prepend-icon="mdiSelectAll"
|
|
:title="tt('Select All')"
|
|
:disabled="!!disabled"
|
|
@click="selectAll"></v-list-item>
|
|
<v-list-item :prepend-icon="mdiSelect"
|
|
:title="tt('Select None')"
|
|
:disabled="!!disabled"
|
|
@click="selectNone"></v-list-item>
|
|
<v-list-item :prepend-icon="mdiSelectInverse"
|
|
:title="tt('Invert Selection')"
|
|
:disabled="!!disabled"
|
|
@click="selectInvert"></v-list-item>
|
|
<v-divider class="my-2"/>
|
|
<v-list-item :prepend-icon="mdiSelectAll"
|
|
:title="tt('Select All in This Page')"
|
|
:disabled="!!disabled"
|
|
@click="selectAllInThisPage"></v-list-item>
|
|
<v-list-item :prepend-icon="mdiSelect"
|
|
:title="tt('Select None in This Page')"
|
|
:disabled="!!disabled"
|
|
@click="selectNoneInThisPage"></v-list-item>
|
|
<v-list-item :prepend-icon="mdiSelectInverse"
|
|
:title="tt('Invert Selection in This Page')"
|
|
:disabled="!!disabled"
|
|
@click="selectInvertInThisPage"></v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-checkbox>
|
|
</template>
|
|
<template #item.data-table-select="{ item }">
|
|
<v-checkbox density="compact"
|
|
:color="!item.valid ? 'error' : 'primary'"
|
|
:disabled="!!disabled"
|
|
v-model="item.selected"></v-checkbox>
|
|
</template>
|
|
<template #item.valid="{ item }">
|
|
<v-icon size="small" :class="{ 'text-error': !item.valid }"
|
|
:disabled="!!disabled"
|
|
:icon="editingTransaction === item ? mdiCheck : mdiPencilOutline"
|
|
@click="editTransaction(item)">
|
|
</v-icon>
|
|
<v-tooltip activator="parent" v-if="!disabled">{{ tt('Edit') }}</v-tooltip>
|
|
</template>
|
|
<template #item.time="{ item }">
|
|
<span>{{ getDisplayDateTime(item) }}</span>
|
|
<v-chip class="ms-1" variant="flat" color="grey" size="x-small"
|
|
v-if="item.utcOffset !== currentTimezoneOffsetMinutes">{{ getDisplayTimezone(item) }}</v-chip>
|
|
</template>
|
|
<template #item.type="{ value }">
|
|
<v-chip label color="secondary" variant="outlined" size="x-small" v-if="value === TransactionType.ModifyBalance">{{ tt('Modify Balance') }}</v-chip>
|
|
<v-chip label class="text-income" variant="outlined" size="x-small" v-else-if="value === TransactionType.Income">{{ tt('Income') }}</v-chip>
|
|
<v-chip label class="text-expense" variant="outlined" size="x-small" v-else-if="value === TransactionType.Expense">{{ tt('Expense') }}</v-chip>
|
|
<v-chip label color="primary" variant="outlined" size="x-small" v-else-if="value === TransactionType.Transfer">{{ tt('Transfer') }}</v-chip>
|
|
<v-chip label color="default" variant="outlined" size="x-small" v-else>{{ tt('Unknown') }}</v-chip>
|
|
</template>
|
|
<template #item.actualCategoryName="{ item }">
|
|
<div class="d-flex align-center" v-if="editingTransaction !== item || item.type === TransactionType.ModifyBalance">
|
|
<span v-if="item.type === TransactionType.ModifyBalance">-</span>
|
|
<ItemIcon size="24px" icon-type="category"
|
|
:icon-id="allCategoriesMap[item.categoryId].icon"
|
|
:color="allCategoriesMap[item.categoryId].color"
|
|
v-if="item.type !== TransactionType.ModifyBalance && item.categoryId && item.categoryId !== '0' && allCategoriesMap[item.categoryId]"></ItemIcon>
|
|
<span class="ms-2" v-if="item.type !== TransactionType.ModifyBalance && item.categoryId && item.categoryId !== '0' && allCategoriesMap[item.categoryId]">
|
|
{{ allCategoriesMap[item.categoryId].name }}
|
|
</span>
|
|
<div class="text-error font-italic" v-else-if="item.type !== TransactionType.ModifyBalance && (!item.categoryId || item.categoryId === '0' || !allCategoriesMap[item.categoryId])">
|
|
<v-icon class="me-1" :icon="mdiAlertOutline"/>
|
|
<span>{{ item.originalCategoryName }}</span>
|
|
</div>
|
|
</div>
|
|
<div style="width: 260px" v-if="editingTransaction === item && item.type === TransactionType.Expense">
|
|
<two-column-select density="compact" variant="plain"
|
|
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="!!disabled || !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(item.categoryId, allCategories[CategoryType.Expense])"
|
|
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(item.categoryId, allCategories[CategoryType.Expense])"
|
|
:placeholder="tt('Category')"
|
|
:items="allCategories[CategoryType.Expense]"
|
|
v-model="item.categoryId">
|
|
</two-column-select>
|
|
</div>
|
|
<div style="width: 260px" v-if="editingTransaction === item && item.type === TransactionType.Income">
|
|
<two-column-select density="compact" variant="plain"
|
|
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="!!disabled || !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(item.categoryId, allCategories[CategoryType.Income])"
|
|
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(item.categoryId, allCategories[CategoryType.Income])"
|
|
:placeholder="tt('Category')"
|
|
:items="allCategories[CategoryType.Income]"
|
|
v-model="item.categoryId">
|
|
</two-column-select>
|
|
</div>
|
|
<div style="width: 260px" v-if="editingTransaction === item && item.type === TransactionType.Transfer">
|
|
<two-column-select density="compact" variant="plain"
|
|
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="!!disabled || !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(item.categoryId, allCategories[CategoryType.Transfer])"
|
|
:custom-selection-secondary-text="getTransactionSecondaryCategoryName(item.categoryId, allCategories[CategoryType.Transfer])"
|
|
:placeholder="tt('Category')"
|
|
:items="allCategories[CategoryType.Transfer]"
|
|
v-model="item.categoryId">
|
|
</two-column-select>
|
|
</div>
|
|
</template>
|
|
<template #item.sourceAmount="{ item }">
|
|
<span>{{ getTransactionDisplayAmount(item) }}</span>
|
|
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId"></v-icon>
|
|
<span v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId">{{ getTransactionDisplayDestinationAmount(item) }}</span>
|
|
</template>
|
|
<template #item.actualSourceAccountName="{ 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>
|
|
<v-icon class="me-1" :icon="mdiAlertOutline"/>
|
|
<span>{{ item.originalSourceAccountName }}</span>
|
|
</div>
|
|
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
|
|
<span v-if="item.type === TransactionType.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 === TransactionType.Transfer && (!item.destinationAccountId || item.destinationAccountId === '0' || !allAccountsMap[item.destinationAccountId])">
|
|
<v-icon class="me-1" :icon="mdiAlertOutline"/>
|
|
<span>{{ item.originalDestinationAccountName }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-center" style="width: 200px" v-if="editingTransaction === item">
|
|
<two-column-select density="compact" variant="plain"
|
|
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="!!disabled || !allVisibleAccounts.length"
|
|
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
|
:custom-selection-primary-text="getSourceAccountDisplayName(item)"
|
|
:placeholder="getSourceAccountTitle(item)"
|
|
:items="allVisibleCategorizedAccounts"
|
|
v-model="item.sourceAccountId">
|
|
</two-column-select>
|
|
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
|
|
<two-column-select density="compact" variant="plain"
|
|
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="!!disabled || !allVisibleAccounts.length"
|
|
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
|
:custom-selection-primary-text="getDestinationAccountDisplayName(item)"
|
|
:placeholder="tt('Destination Account')"
|
|
:items="allVisibleCategorizedAccounts"
|
|
v-model="item.destinationAccountId"
|
|
v-if="item.type === TransactionType.Transfer">
|
|
</two-column-select>
|
|
</div>
|
|
</template>
|
|
<template #item.geoLocation="{ item }">
|
|
<span v-if="item.geoLocation">{{ `(${formatCoordinate(item.geoLocation, coordinateDisplayType)})` }}</span>
|
|
<span v-else-if="!item.geoLocation">{{ tt('None') }}</span>
|
|
</template>
|
|
<template #item.tagIds="{ item }">
|
|
<div v-if="editingTransaction !== item">
|
|
<v-chip class="transaction-tag" size="small"
|
|
:class="{ 'font-italic': !tagId || tagId === '0' || !allTagsMap[tagId] }"
|
|
:prepend-icon="tagId && tagId !== '0' && allTagsMap[tagId] ? mdiPound : mdiAlertOutline"
|
|
:color="tagId && tagId !== '0' && allTagsMap[tagId] ? 'default' : 'error'"
|
|
:text="tagId && tagId !== '0' && allTagsMap[tagId] ? allTagsMap[tagId].name : item.originalTagNames[index]"
|
|
:key="tagId"
|
|
v-for="(tagId, index) in item.tagIds"/>
|
|
<v-chip class="transaction-tag" size="small"
|
|
:text="tt('None')"
|
|
v-if="!item.tagIds || !item.tagIds.length"/>
|
|
</div>
|
|
<div style="width: 200px" v-if="editingTransaction === item">
|
|
<v-autocomplete
|
|
item-title="name"
|
|
item-value="id"
|
|
auto-select-first
|
|
persistent-placeholder
|
|
multiple
|
|
chips
|
|
closable-chips
|
|
density="compact" variant="plain"
|
|
:disabled="!!disabled"
|
|
:placeholder="tt('None')"
|
|
:items="allTags"
|
|
:no-data-text="tt('No available tag')"
|
|
v-model="editingTags"
|
|
>
|
|
<template #chip="{ props, index }">
|
|
<v-chip :class="{ 'font-italic': !isTagValid(editingTags, index) }"
|
|
:prepend-icon="isTagValid(editingTags, index) ? mdiPound : mdiAlertOutline"
|
|
:color="isTagValid(editingTags, index) ? 'default' : 'error'"
|
|
:text="isTagValid(editingTags, index) ? allTagsMap[editingTags[index]].name : item.originalTagNames[index]"
|
|
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="mdiPound"/>
|
|
<span>{{ item.title }}</span>
|
|
</div>
|
|
</v-list-item-title>
|
|
</template>
|
|
</v-list-item>
|
|
</template>
|
|
</v-autocomplete>
|
|
</div>
|
|
</template>
|
|
<template #bottom>
|
|
<div class="title-and-toolbar d-flex align-center text-no-wrap mt-2"
|
|
v-if="importTransactions && importTransactions.length > 10">
|
|
<span :class="{ 'text-error': selectedInvalidTransactionCount > 0 }">
|
|
{{ tt('format.misc.selectedCount', { count: getDisplayCount(selectedImportTransactionCount), totalCount: getDisplayCount(importTransactions.length) }) }}
|
|
</span>
|
|
<v-spacer/>
|
|
<span>{{ tt('Transactions Per Page') }}</span>
|
|
<v-select class="ms-2" density="compact" max-width="100"
|
|
item-title="name"
|
|
item-value="value"
|
|
:disabled="!!disabled"
|
|
:items="importTransactionsTablePageOptions"
|
|
v-model="countPerPage"
|
|
/>
|
|
<pagination-buttons density="compact"
|
|
:disabled="!!disabled"
|
|
:totalPageCount="totalPageCount"
|
|
v-model="currentPage"></pagination-buttons>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
|
|
<v-dialog width="640" v-model="showCustomDescriptionDialog">
|
|
<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">{{ tt('Filter Description') }}</h4>
|
|
</div>
|
|
</template>
|
|
<v-card-text class="mb-md-4 w-100 d-flex justify-center">
|
|
<v-text-field
|
|
type="text"
|
|
persistent-placeholder
|
|
:label="tt('Description')"
|
|
:placeholder="tt('Description')"
|
|
v-model="currentDescriptionFilterValue"
|
|
/>
|
|
</v-card-text>
|
|
<v-card-text class="overflow-y-visible">
|
|
<div class="w-100 d-flex justify-center gap-4">
|
|
<v-btn :disabled="!currentDescriptionFilterValue" @click="showCustomDescriptionDialog = false; filters.description = currentDescriptionFilterValue">{{ tt('OK') }}</v-btn>
|
|
<v-btn color="secondary" variant="tonal" @click="showCustomDescriptionDialog = false; currentDescriptionFilterValue = ''">{{ tt('Cancel') }}</v-btn>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<date-range-selection-dialog :title="tt('Custom Date Range')"
|
|
:min-time="filters.minDatetime"
|
|
:max-time="filters.maxDatetime"
|
|
v-model:show="showCustomDateRangeDialog"
|
|
@dateRange:change="changeCustomDateFilter"
|
|
@error="onShowDateRangeError" />
|
|
<batch-replace-dialog ref="batchReplaceDialog" />
|
|
<batch-replace-all-types-dialog ref="batchReplaceAllTypesDialog" />
|
|
<batch-create-dialog ref="batchCreateDialog" />
|
|
<snack-bar ref="snackbar" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import PaginationButtons from '@/components/desktop/PaginationButtons.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 } from 'vue';
|
|
|
|
import { useI18n } from '@/locales/helpers.ts';
|
|
|
|
import { useSettingsStore } from '@/stores/setting.ts';
|
|
import { useUserStore } from '@/stores/user.ts';
|
|
import { useAccountsStore } from '@/stores/account.ts';
|
|
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
|
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
|
|
|
import { type NameValue, type NameNumeralValue, itemAndIndex, reversed, keys } from '@/core/base.ts';
|
|
import { type NumeralSystem } from '@/core/numeral.ts';
|
|
import { CategoryType } from '@/core/category.ts';
|
|
import { TransactionType } from '@/core/transaction.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 { ImportTransaction } from '@/models/imported_transaction.ts';
|
|
|
|
import {
|
|
isString,
|
|
isNumber,
|
|
objectFieldToArrayItem
|
|
} from '@/lib/common.ts';
|
|
import {
|
|
getUtcOffsetByUtcOffsetMinutes,
|
|
getTimezoneOffsetMinutes
|
|
} from '@/lib/datetime.ts';
|
|
import { formatCoordinate } from '@/lib/coordinate.ts';
|
|
import {
|
|
getAccountMapByName
|
|
} from '@/lib/account.ts';
|
|
import {
|
|
transactionTypeToCategoryType,
|
|
getSecondaryTransactionMapByName,
|
|
getTransactionPrimaryCategoryName,
|
|
getTransactionSecondaryCategoryName
|
|
} from '@/lib/category.ts';
|
|
|
|
import {
|
|
mdiCheck,
|
|
mdiArrowRight,
|
|
mdiSelectAll,
|
|
mdiSelect,
|
|
mdiSelectInverse,
|
|
mdiPencilOutline,
|
|
mdiAlertOutline,
|
|
mdiPound,
|
|
mdiFindReplace,
|
|
mdiShapePlusOutline,
|
|
mdiTransfer
|
|
} from '@mdi/js';
|
|
|
|
type SnackBarType = InstanceType<typeof SnackBar>;
|
|
type BatchReplaceDialogType = InstanceType<typeof BatchReplaceDialog>;
|
|
type BatchReplaceAllTypesDialogType = InstanceType<typeof BatchReplaceAllTypesDialog>;
|
|
type BatchCreateDialogType = InstanceType<typeof BatchCreateDialog>;
|
|
|
|
interface ImportTransactionCheckDataFilter {
|
|
minDatetime: number | null; // minDatetime or maxDatetime is null for 'All Date Range', all are not null for 'Custom Date Range'
|
|
maxDatetime: number | null;
|
|
transactionType: TransactionType | null; // null for 'All Transaction Type'
|
|
category: string | null | undefined; // null for 'All Category', undefined for 'Invalid Category'
|
|
account: string | null | undefined; // null for 'All Account', undefined for 'Invalid Account'
|
|
tag: string | null | undefined; // null for 'All Tag', undefined for 'Invalid Tag'
|
|
description: string | null; // null for 'All Description'
|
|
}
|
|
|
|
interface ImportTransactionCheckDataMenuGroup {
|
|
title: string;
|
|
items: ImportTransactionCheckDataMenu[];
|
|
}
|
|
|
|
interface ImportTransactionCheckDataMenu {
|
|
prependIcon?: string;
|
|
title: string;
|
|
subTitle?: string;
|
|
appendIcon?: string;
|
|
disabled?: boolean;
|
|
divider?: boolean;
|
|
onClick: () => void;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
importTransactions?: ImportTransaction[]
|
|
disabled?: boolean;
|
|
}>();
|
|
|
|
const {
|
|
tt,
|
|
getCurrentNumeralSystemType,
|
|
formatUnixTimeToLongDateTime,
|
|
formatAmountToLocalizedNumeralsWithCurrency,
|
|
getCategorizedAccountsWithDisplayBalance
|
|
} = useI18n();
|
|
|
|
const settingsStore = useSettingsStore();
|
|
const userStore = useUserStore();
|
|
const accountsStore = useAccountsStore();
|
|
const transactionCategoriesStore = useTransactionCategoriesStore();
|
|
const transactionTagsStore = useTransactionTagsStore();
|
|
|
|
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
|
const batchReplaceDialog = useTemplateRef<BatchReplaceDialogType>('batchReplaceDialog');
|
|
const batchReplaceAllTypesDialog = useTemplateRef<BatchReplaceAllTypesDialogType>('batchReplaceAllTypesDialog');
|
|
const batchCreateDialog = useTemplateRef<BatchCreateDialogType>('batchCreateDialog');
|
|
|
|
const editingTransaction = ref<ImportTransaction | null>(null);
|
|
const editingTags = ref<string[]>([]);
|
|
const filters = ref<ImportTransactionCheckDataFilter>({
|
|
minDatetime: null,
|
|
maxDatetime: null,
|
|
transactionType: null,
|
|
category: null,
|
|
account: null,
|
|
tag: null,
|
|
description: null
|
|
});
|
|
|
|
const currentPage = ref<number>(1);
|
|
const countPerPage = ref<number>(10);
|
|
const showCustomDateRangeDialog = ref<boolean>(false);
|
|
const showCustomDescriptionDialog = ref<boolean>(false);
|
|
const currentDescriptionFilterValue = ref<string | null>(null);
|
|
|
|
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
|
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
|
|
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone));
|
|
|
|
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
|
const coordinateDisplayType = computed<number>(() => userStore.currentUserCoordinateDisplayType);
|
|
|
|
const allAccounts = computed<Account[]>(() => accountsStore.allPlainAccounts);
|
|
const allVisibleAccounts = computed<Account[]>(() => accountsStore.allVisiblePlainAccounts);
|
|
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value));
|
|
const allAccountsMap = computed<Record<string, Account>>(() => accountsStore.allAccountsMap);
|
|
const allAccountsMapByName = computed<Record<string, Account>>(() => getAccountMapByName(accountsStore.allAccounts));
|
|
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
|
|
const allCategoriesMap = computed<Record<string, TransactionCategory>>(() => transactionCategoriesStore.allTransactionCategoriesMap);
|
|
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
|
const allTagsMap = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
|
|
|
|
const hasAvailableExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableExpenseCategories);
|
|
const hasAvailableIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableIncomeCategories);
|
|
const hasAvailableTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasAvailableTransferCategories);
|
|
|
|
const isEditing = computed<boolean>(() => !!editingTransaction.value);
|
|
const canImport = computed<boolean>(() => selectedImportTransactionCount.value > 0 && selectedInvalidTransactionCount.value < 1);
|
|
|
|
const filterMenus = computed<ImportTransactionCheckDataMenuGroup[]>(() => [
|
|
{
|
|
title: tt('Date Range'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.minDatetime === null || filters.value.maxDatetime === null ? mdiCheck : undefined,
|
|
onClick: () => {
|
|
filters.value.minDatetime = null;
|
|
filters.value.maxDatetime = null;
|
|
}
|
|
},
|
|
{
|
|
title: tt('Custom'),
|
|
subTitle: displayFilterCustomDateRange.value,
|
|
appendIcon: filters.value.minDatetime !== null && filters.value.maxDatetime !== null ? mdiCheck : undefined,
|
|
onClick: () => showCustomDateRangeDialog.value = true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: tt('Type'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.transactionType === null ? mdiCheck : undefined,
|
|
onClick: () => filters.value.transactionType = null
|
|
},
|
|
{
|
|
title: tt('Income'),
|
|
appendIcon: filters.value.transactionType === TransactionType.Income ? mdiCheck : undefined,
|
|
onClick: () => filters.value.transactionType = TransactionType.Income
|
|
},
|
|
{
|
|
title: tt('Expense'),
|
|
appendIcon: filters.value.transactionType === TransactionType.Expense ? mdiCheck : undefined,
|
|
onClick: () => filters.value.transactionType = TransactionType.Expense
|
|
},
|
|
{
|
|
title: tt('Transfer'),
|
|
appendIcon: filters.value.transactionType === TransactionType.Transfer ? mdiCheck : undefined,
|
|
onClick: () => filters.value.transactionType = TransactionType.Transfer
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: tt('Category'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.category === null ? mdiCheck : undefined,
|
|
onClick: () => filters.value.category = null
|
|
},
|
|
{
|
|
title: tt('Invalid Category'),
|
|
appendIcon: filters.value.category === undefined ? mdiCheck : undefined,
|
|
onClick: () => filters.value.category = undefined
|
|
},
|
|
{
|
|
title: tt('None'),
|
|
appendIcon: filters.value.category === '' ? mdiCheck : undefined,
|
|
onClick: () => filters.value.category = ''
|
|
},
|
|
...allUsedCategoryNames.value.map(name => ({
|
|
title: name,
|
|
appendIcon: filters.value.category === name ? mdiCheck : undefined,
|
|
onClick: () => filters.value.category = name
|
|
}))
|
|
]
|
|
},
|
|
{
|
|
title: tt('Account'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.account === null ? mdiCheck : undefined,
|
|
onClick: () => filters.value.account = null
|
|
},
|
|
{
|
|
title: tt('Invalid Account'),
|
|
appendIcon: filters.value.account === undefined ? mdiCheck : undefined,
|
|
onClick: () => filters.value.account = undefined
|
|
},
|
|
{
|
|
title: tt('None'),
|
|
appendIcon: filters.value.account === '' ? mdiCheck : undefined,
|
|
onClick: () => filters.value.account = ''
|
|
},
|
|
...allUsedAccountNames.value.map(name => ({
|
|
title: name,
|
|
appendIcon: filters.value.account === name ? mdiCheck : undefined,
|
|
onClick: () => filters.value.account = name
|
|
}))
|
|
]
|
|
},
|
|
{
|
|
title: tt('Tags'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.tag === null ? mdiCheck : undefined,
|
|
onClick: () => filters.value.tag = null
|
|
},
|
|
{
|
|
title: tt('Invalid Tag'),
|
|
appendIcon: filters.value.tag === undefined ? mdiCheck : undefined,
|
|
onClick: () => filters.value.tag = undefined
|
|
},
|
|
{
|
|
title: tt('None'),
|
|
appendIcon: filters.value.tag === '' ? mdiCheck : undefined,
|
|
onClick: () => filters.value.tag = ''
|
|
},
|
|
...allUsedTagNames.value.map(name => ({
|
|
title: name,
|
|
appendIcon: filters.value.tag === name ? mdiCheck : undefined,
|
|
onClick: () => filters.value.tag = name
|
|
}))
|
|
]
|
|
},
|
|
{
|
|
title: tt('Description'),
|
|
items: [
|
|
{
|
|
title: tt('All'),
|
|
appendIcon: filters.value.description === null ? mdiCheck : undefined,
|
|
onClick: () => filters.value.description = null
|
|
},
|
|
{
|
|
title: tt('None'),
|
|
appendIcon: filters.value.description === '' ? mdiCheck : undefined,
|
|
onClick: () => filters.value.description = ''
|
|
},
|
|
{
|
|
title: tt('Custom'),
|
|
subTitle: filters.value.description !== null ? filters.value.description : undefined,
|
|
appendIcon: filters.value.description !== null && filters.value.description !== '' ? mdiCheck : undefined,
|
|
onClick: () => {
|
|
currentDescriptionFilterValue.value = filters.value.description || '';
|
|
showCustomDescriptionDialog.value = true;
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]);
|
|
|
|
const toolMenus = computed<ImportTransactionCheckDataMenu[]>(() => [
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Expense Categories'),
|
|
disabled: isEditing.value || selectedExpenseTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('expenseCategory')
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Income Categories'),
|
|
disabled: isEditing.value || selectedIncomeTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('incomeCategory')
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Transfer Categories'),
|
|
disabled: isEditing.value || selectedTransferTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('transferCategory')
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Accounts'),
|
|
disabled: isEditing.value || selectedImportTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('account')
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Destination Accounts'),
|
|
disabled: isEditing.value || selectedTransferTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('destinationAccount')
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Selected Transaction Tags'),
|
|
disabled: isEditing.value || selectedImportTransactionCount.value < 1,
|
|
onClick: () => showBatchReplaceDialog('tag', allOriginalTransactionTagNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Replace Invalid Expense Categories'),
|
|
disabled: isEditing.value || !allInvalidExpenseCategoryNames.value || allInvalidExpenseCategoryNames.value.length < 1,
|
|
divider: true,
|
|
onClick: () => showReplaceInvalidItemDialog('expenseCategory', allInvalidExpenseCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Replace Invalid Income Categories'),
|
|
disabled: isEditing.value || !allInvalidIncomeCategoryNames.value || allInvalidIncomeCategoryNames.value.length < 1,
|
|
onClick: () => showReplaceInvalidItemDialog('incomeCategory', allInvalidIncomeCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Replace Invalid Transfer Categories'),
|
|
disabled: isEditing.value || !allInvalidTransferCategoryNames.value || allInvalidTransferCategoryNames.value.length < 1,
|
|
onClick: () => showReplaceInvalidItemDialog('transferCategory', allInvalidTransferCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Replace Invalid Accounts'),
|
|
disabled: isEditing.value || !allInvalidAccountNames.value || allInvalidAccountNames.value.length < 1,
|
|
onClick: () => showReplaceInvalidItemDialog('account', allInvalidAccountNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Replace Invalid Transaction Tags'),
|
|
disabled: isEditing.value || !allInvalidTransactionTagNames.value || allInvalidTransactionTagNames.value.length < 1,
|
|
onClick: () => showReplaceInvalidItemDialog('tag', allInvalidTransactionTagNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiFindReplace,
|
|
title: tt('Batch Replace Categories / Accounts / Tags'),
|
|
disabled: isEditing.value,
|
|
divider: true,
|
|
onClick: showReplaceAllTypesDialog
|
|
},
|
|
{
|
|
prependIcon: mdiShapePlusOutline,
|
|
title: tt('Create Nonexistent Expense Categories'),
|
|
disabled: isEditing.value || !allInvalidExpenseCategoryNames.value || allInvalidExpenseCategoryNames.value.length < 1,
|
|
divider: true,
|
|
onClick: () => showBatchCreateInvalidItemDialog('expenseCategory', allInvalidExpenseCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiShapePlusOutline,
|
|
title: tt('Create Nonexistent Income Categories'),
|
|
disabled: isEditing.value || !allInvalidIncomeCategoryNames.value || allInvalidIncomeCategoryNames.value.length < 1,
|
|
onClick: () => showBatchCreateInvalidItemDialog('incomeCategory', allInvalidIncomeCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiShapePlusOutline,
|
|
title: tt('Create Nonexistent Transfer Categories'),
|
|
disabled: isEditing.value || !allInvalidTransferCategoryNames.value || allInvalidTransferCategoryNames.value.length < 1,
|
|
onClick: () => showBatchCreateInvalidItemDialog('transferCategory', allInvalidTransferCategoryNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiShapePlusOutline,
|
|
title: tt('Create Nonexistent Transaction Tags'),
|
|
disabled: isEditing.value || !allInvalidTransactionTagNames.value || allInvalidTransactionTagNames.value.length < 1,
|
|
onClick: () => showBatchCreateInvalidItemDialog('tag', allInvalidTransactionTagNames.value)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Expense Transaction to Income Transaction'),
|
|
disabled: isEditing.value || selectedExpenseTransactionCount.value < 1,
|
|
divider: true,
|
|
onClick: () => convertTransactionType(TransactionType.Expense, TransactionType.Income)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Expense Transaction to Transfer Transaction'),
|
|
disabled: isEditing.value || selectedExpenseTransactionCount.value < 1,
|
|
onClick: () => convertTransactionType(TransactionType.Expense, TransactionType.Transfer)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Income Transaction to Expense Transaction'),
|
|
disabled: isEditing.value || selectedIncomeTransactionCount.value < 1,
|
|
onClick: () => convertTransactionType(TransactionType.Income, TransactionType.Expense)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Income Transaction to Transfer Transaction'),
|
|
disabled: isEditing.value || selectedIncomeTransactionCount.value < 1,
|
|
onClick: () => convertTransactionType(TransactionType.Income, TransactionType.Transfer)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Transfer Transaction to Expense Transaction'),
|
|
disabled: isEditing.value || selectedTransferTransactionCount.value < 1,
|
|
onClick: () => convertTransactionType(TransactionType.Transfer, TransactionType.Expense)
|
|
},
|
|
{
|
|
prependIcon: mdiTransfer,
|
|
title: tt('Batch Convert Transfer Transaction to Income Transaction'),
|
|
disabled: isEditing.value || selectedTransferTransactionCount.value < 1,
|
|
onClick: () => convertTransactionType(TransactionType.Transfer, TransactionType.Income)
|
|
}
|
|
]);
|
|
|
|
const importTransactionsTableHeight = computed<number | undefined>(() => {
|
|
if (countPerPage.value <= 10 || !props.importTransactions || props.importTransactions.length <= 10) {
|
|
return undefined;
|
|
} else {
|
|
return 400;
|
|
}
|
|
});
|
|
|
|
const importTransactionHeaders = computed<object[]>(() => {
|
|
return [
|
|
{ value: 'valid', sortable: true, nowrap: true, width: 35 },
|
|
{ value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true, maxWidth: 280 },
|
|
{ value: 'type', title: tt('Type'), sortable: true, nowrap: true, maxWidth: 140 },
|
|
{ value: 'actualCategoryName', title: tt('Category'), sortable: true, nowrap: true },
|
|
{ value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true },
|
|
{ value: 'actualSourceAccountName', title: tt('Account'), sortable: true, nowrap: true },
|
|
{ value: 'geoLocation', title: tt('Geographic Location'), sortable: true, nowrap: true },
|
|
{ value: 'tagIds', title: tt('Tags'), sortable: true, nowrap: true },
|
|
{ value: 'comment', title: tt('Description'), sortable: true, nowrap: true },
|
|
];
|
|
});
|
|
|
|
const importTransactionsTablePageOptions = computed<NameNumeralValue[]>(() => getTablePageOptions(props.importTransactions?.length));
|
|
|
|
const totalPageCount = computed<number>(() => {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return 1;
|
|
}
|
|
|
|
let count = 0;
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (isTransactionDisplayed(importTransaction)) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return Math.ceil(count / countPerPage.value);
|
|
});
|
|
|
|
const currentPageTransactions = computed<ImportTransaction[]>(() => {
|
|
const ret: ImportTransaction[] = [];
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return ret;
|
|
}
|
|
|
|
const previousCount = Math.max(0, (currentPage.value - 1) * countPerPage.value);
|
|
let count = 0;
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (ret.length >= countPerPage.value) {
|
|
break;
|
|
}
|
|
|
|
if (isTransactionDisplayed(importTransaction)) {
|
|
if (count >= previousCount) {
|
|
ret.push(importTransaction);
|
|
}
|
|
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
|
|
const selectedImportTransactionCount = computed<number>(() => {
|
|
let count = 0;
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return count;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.selected) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
});
|
|
|
|
const selectedExpenseTransactionCount = computed<number>(() => {
|
|
let count = 0;
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return count;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.selected && importTransaction.type === TransactionType.Expense) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
});
|
|
|
|
const selectedIncomeTransactionCount = computed<number>(() => {
|
|
let count = 0;
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return count;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.selected && importTransaction.type === TransactionType.Income) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
});
|
|
|
|
const selectedTransferTransactionCount = computed<number>(() => {
|
|
let count = 0;
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return count;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.selected && importTransaction.type === TransactionType.Transfer) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
});
|
|
|
|
const selectedInvalidTransactionCount = computed<number>(() => {
|
|
let count = 0;
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return count;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.valid && importTransaction.selected) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
});
|
|
|
|
const anyButNotAllTransactionSelected = computed<boolean>(() => !!props.importTransactions && selectedImportTransactionCount.value > 0 && selectedImportTransactionCount.value !== props.importTransactions.length);
|
|
const allTransactionSelected = computed<boolean>(() => !!props.importTransactions && selectedImportTransactionCount.value === props.importTransactions.length);
|
|
|
|
const allUsedCategoryNames = computed<string[]>(() => {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return [];
|
|
}
|
|
|
|
const categoryNames: Record<string, boolean> = {};
|
|
|
|
for (const transaction of props.importTransactions) {
|
|
if (transaction.actualCategoryName && transaction.actualCategoryName !== '') {
|
|
categoryNames[transaction.actualCategoryName] = true;
|
|
}
|
|
}
|
|
|
|
return objectFieldToArrayItem(categoryNames);
|
|
});
|
|
|
|
const allUsedAccountNames = computed<string[]>(() => {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return [];
|
|
}
|
|
|
|
const accountNames: Record<string, boolean> = {};
|
|
|
|
for (const transaction of props.importTransactions) {
|
|
if (transaction.actualSourceAccountName && transaction.actualSourceAccountName !== '') {
|
|
accountNames[transaction.actualSourceAccountName] = true;
|
|
}
|
|
|
|
if (transaction.actualDestinationAccountName && transaction.actualDestinationAccountName !== '') {
|
|
accountNames[transaction.actualDestinationAccountName] = true;
|
|
}
|
|
}
|
|
|
|
return objectFieldToArrayItem(accountNames);
|
|
});
|
|
|
|
const allUsedTagNames = computed<string[]>(() => {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return [];
|
|
}
|
|
|
|
const tagNames: Record<string, boolean> = {};
|
|
|
|
for (const transaction of props.importTransactions) {
|
|
if (!transaction.tagIds || !transaction.originalTagNames) {
|
|
continue;
|
|
}
|
|
|
|
for (const [tagId, tagIndex] of itemAndIndex(transaction.tagIds)) {
|
|
const originalTagName = transaction.originalTagNames[tagIndex] as string | undefined;
|
|
|
|
if (tagId && tagId !== '0' && allTagsMap.value[tagId] && allTagsMap.value[tagId].name) {
|
|
tagNames[allTagsMap.value[tagId].name] = true;
|
|
} else if (originalTagName) {
|
|
tagNames[originalTagName] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return objectFieldToArrayItem(tagNames);
|
|
});
|
|
|
|
const allInvalidExpenseCategoryNames = computed<NameValue[]>(() => getCurrentInvalidCategoryNames(TransactionType.Expense));
|
|
const allInvalidIncomeCategoryNames = computed<NameValue[]>(() => getCurrentInvalidCategoryNames(TransactionType.Income));
|
|
const allInvalidTransferCategoryNames = computed<NameValue[]>(() => getCurrentInvalidCategoryNames(TransactionType.Transfer));
|
|
const allInvalidAccountNames = computed<NameValue[]>(() => getCurrentInvalidAccountNames());
|
|
const allInvalidTransactionTagNames = computed<NameValue[]>(() => getCurrentInvalidTagNames());
|
|
const allOriginalTransactionTagNames = computed<NameValue[]>(() => getAllOriginalTagNames());
|
|
|
|
const displayFilterCustomDateRange = computed<string>(() => {
|
|
if (filters.value.minDatetime === null || filters.value.maxDatetime === null) {
|
|
return '';
|
|
}
|
|
|
|
const minDisplayTime = formatUnixTimeToLongDateTime(filters.value.minDatetime);
|
|
const maxDisplayTime = formatUnixTimeToLongDateTime(filters.value.maxDatetime);
|
|
|
|
return `${minDisplayTime} - ${maxDisplayTime}`
|
|
});
|
|
|
|
function getDisplayCount(count: number): string {
|
|
return numeralSystem.value.formatNumber(count);
|
|
}
|
|
|
|
function getTablePageOptions(linesCount?: number): NameNumeralValue[] {
|
|
const pageOptions: NameNumeralValue[] = [];
|
|
|
|
if (!linesCount || linesCount < 1) {
|
|
pageOptions.push({ value: -1, name: tt('All') });
|
|
return pageOptions;
|
|
}
|
|
|
|
for (const count of [ 5, 10, 15, 20, 25, 30, 50 ]) {
|
|
if (linesCount < count) {
|
|
break;
|
|
}
|
|
|
|
pageOptions.push({ value: count, name: getDisplayCount(count) });
|
|
}
|
|
|
|
pageOptions.push({ value: -1, name: tt('All') });
|
|
|
|
return pageOptions;
|
|
}
|
|
|
|
function isTransactionDisplayed(transaction: ImportTransaction): boolean {
|
|
if (isNumber(filters.value.minDatetime) && isNumber(filters.value.maxDatetime) && (transaction.time < filters.value.minDatetime || transaction.time > filters.value.maxDatetime)) {
|
|
return false;
|
|
}
|
|
|
|
if (isNumber(filters.value.transactionType) && transaction.type !== filters.value.transactionType) {
|
|
return false;
|
|
}
|
|
|
|
if (isString(filters.value.category)) {
|
|
if (filters.value.category === '' && transaction.actualCategoryName !== '') {
|
|
return false;
|
|
} else if (filters.value.category !== '' && transaction.actualCategoryName !== filters.value.category) {
|
|
return false;
|
|
}
|
|
} else if (filters.value.category === undefined) {
|
|
if (transaction.type !== TransactionType.ModifyBalance && transaction.categoryId && transaction.categoryId !== '0') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isString(filters.value.account)) {
|
|
if (filters.value.account === '' && (transaction.actualSourceAccountName !== '' || transaction.actualDestinationAccountName !== '')) {
|
|
return false;
|
|
} else if (filters.value.account !== '' && transaction.actualSourceAccountName !== filters.value.account && transaction.actualDestinationAccountName !== filters.value.account) {
|
|
return false;
|
|
}
|
|
} else if (filters.value.account === undefined) {
|
|
if (transaction.type !== TransactionType.Transfer && transaction.sourceAccountId && transaction.sourceAccountId !== '0') {
|
|
return false;
|
|
} else if (transaction.type === TransactionType.Transfer && transaction.sourceAccountId && transaction.sourceAccountId !== '0' && transaction.destinationAccountId && transaction.destinationAccountId !== '0') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isString(filters.value.tag)) {
|
|
if (filters.value.tag === '' && transaction.tagIds && transaction.tagIds.length) {
|
|
return false;
|
|
} else if (filters.value.tag !== '') {
|
|
let hasTagName = false;
|
|
|
|
if (transaction.tagIds && transaction.tagIds.length) {
|
|
for (const [tagId, tagIndex] of itemAndIndex(transaction.tagIds)) {
|
|
let tagName: string = transaction.originalTagNames ? (transaction.originalTagNames[tagIndex] ?? '') : '';
|
|
|
|
if (tagId && tagId !== '0' && allTagsMap.value[tagId] && allTagsMap.value[tagId].name) {
|
|
tagName = allTagsMap.value[tagId].name;
|
|
}
|
|
|
|
if (tagName === filters.value.tag) {
|
|
hasTagName = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasTagName) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (filters.value.tag === undefined) {
|
|
if (transaction.tagIds && transaction.tagIds.length) {
|
|
let hasInvalidTag = false;
|
|
|
|
for (const tagId of transaction.tagIds) {
|
|
if (!tagId || tagId === '0') {
|
|
hasInvalidTag = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasInvalidTag) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isString(filters.value.description)) {
|
|
if (filters.value.description === '' && transaction.comment !== '') {
|
|
return false;
|
|
} else if (filters.value.description !== '' && transaction.comment.indexOf(filters.value.description) < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function isTagValid(tagIds: string[], tagIndex: number): boolean {
|
|
if (!tagIds || !tagIds[tagIndex]) {
|
|
return false;
|
|
}
|
|
|
|
if (tagIds[tagIndex] === '0') {
|
|
return false;
|
|
}
|
|
|
|
const tagId = tagIds[tagIndex];
|
|
return !!allTagsMap.value[tagId];
|
|
}
|
|
|
|
function getDisplayDateTime(transaction: ImportTransaction): string {
|
|
return formatUnixTimeToLongDateTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value);
|
|
}
|
|
|
|
function getDisplayTimezone(transaction: ImportTransaction): string {
|
|
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
|
|
}
|
|
|
|
function getDisplayCurrency(value: number, currencyCode: string): string {
|
|
return formatAmountToLocalizedNumeralsWithCurrency(value, currencyCode);
|
|
}
|
|
|
|
function getTransactionDisplayAmount(transaction: ImportTransaction): string {
|
|
let currency = transaction.originalSourceAccountCurrency || defaultCurrency.value;
|
|
|
|
if (transaction.sourceAccountId && transaction.sourceAccountId !== '0' && allAccountsMap.value[transaction.sourceAccountId]) {
|
|
currency = allAccountsMap.value[transaction.sourceAccountId].currency;
|
|
}
|
|
|
|
return getDisplayCurrency(transaction.sourceAmount, currency);
|
|
}
|
|
|
|
function getTransactionDisplayDestinationAmount(transaction: ImportTransaction): string {
|
|
if (transaction.type !== TransactionType.Transfer) {
|
|
return '-';
|
|
}
|
|
|
|
let currency = transaction.originalDestinationAccountCurrency || defaultCurrency.value;
|
|
|
|
if (transaction.destinationAccountId && transaction.destinationAccountId !== '0' && allAccountsMap.value[transaction.destinationAccountId]) {
|
|
currency = allAccountsMap.value[transaction.destinationAccountId].currency;
|
|
}
|
|
|
|
return getDisplayCurrency(transaction.destinationAmount, currency);
|
|
}
|
|
|
|
function getSourceAccountTitle(transaction: ImportTransaction): string {
|
|
if (transaction.type === TransactionType.Expense || transaction.type === TransactionType.Income) {
|
|
return tt('Account');
|
|
} else if (transaction.type === TransactionType.Transfer) {
|
|
return tt('Source Account');
|
|
} else {
|
|
return tt('Account');
|
|
}
|
|
}
|
|
|
|
function getSourceAccountDisplayName(transaction: ImportTransaction): string {
|
|
if (transaction.sourceAccountId) {
|
|
return Account.findAccountNameById(allAccounts.value, transaction.sourceAccountId) || '';
|
|
} else {
|
|
return tt('None');
|
|
}
|
|
}
|
|
|
|
function getDestinationAccountDisplayName(transaction: ImportTransaction): string {
|
|
if (transaction.destinationAccountId) {
|
|
return Account.findAccountNameById(allAccounts.value, transaction.destinationAccountId) || '';
|
|
} else {
|
|
return tt('None');
|
|
}
|
|
}
|
|
|
|
function getCurrentInvalidCategoryNames(transactionType: TransactionType): NameValue[] {
|
|
const invalidCategoryNames: Record<string, boolean> = {};
|
|
const invalidCategories: NameValue[] = [];
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return invalidCategories;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
const categoryId = importTransaction.categoryId;
|
|
|
|
if (importTransaction.type === transactionType && (!categoryId || categoryId === '0' || !allCategoriesMap.value[categoryId])) {
|
|
invalidCategoryNames[importTransaction.originalCategoryName] = true;
|
|
}
|
|
}
|
|
|
|
for (const name of keys(invalidCategoryNames)) {
|
|
invalidCategories.push({
|
|
name: name || tt('(Empty)'),
|
|
value: name
|
|
});
|
|
}
|
|
|
|
return invalidCategories;
|
|
}
|
|
|
|
function getCurrentInvalidAccountNames(): NameValue[] {
|
|
const invalidAccountNames: Record<string, boolean> = {};
|
|
const invalidAccounts: NameValue[] = [];
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return invalidAccounts;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
const sourceAccountId = importTransaction.sourceAccountId;
|
|
const destinationAccountId = importTransaction.destinationAccountId;
|
|
|
|
if (!sourceAccountId || sourceAccountId === '0' || !allAccountsMap.value[sourceAccountId]) {
|
|
invalidAccountNames[importTransaction.originalSourceAccountName] = true;
|
|
}
|
|
|
|
if (importTransaction.type === TransactionType.Transfer && isString(importTransaction.originalDestinationAccountName) && (!destinationAccountId || destinationAccountId === '0' || !allAccountsMap.value[destinationAccountId])) {
|
|
invalidAccountNames[importTransaction.originalDestinationAccountName] = true;
|
|
}
|
|
}
|
|
|
|
for (const name of keys(invalidAccountNames)) {
|
|
invalidAccounts.push({
|
|
name: name || tt('(Empty)'),
|
|
value: name
|
|
});
|
|
}
|
|
|
|
return invalidAccounts;
|
|
}
|
|
|
|
function getCurrentInvalidTagNames(): NameValue[] {
|
|
const invalidTagNames: Record<string, boolean> = {};
|
|
const invalidTags: NameValue[] = [];
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return invalidTags;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.tagIds || !importTransaction.originalTagNames) {
|
|
continue;
|
|
}
|
|
|
|
for (const [tagId, tagIndex] of itemAndIndex(importTransaction.tagIds)) {
|
|
const originalTagName = importTransaction.originalTagNames[tagIndex] as string | undefined;
|
|
|
|
if (!originalTagName) {
|
|
continue;
|
|
}
|
|
|
|
if (!tagId || tagId === '0' || !allTagsMap.value[tagId]) {
|
|
invalidTagNames[originalTagName] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const name of keys(invalidTagNames)) {
|
|
invalidTags.push({
|
|
name: name || tt('(Empty)'),
|
|
value: name
|
|
});
|
|
}
|
|
|
|
return invalidTags;
|
|
}
|
|
|
|
function getAllOriginalTagNames(): NameValue[] {
|
|
const allOriginalTagNames: Record<string, boolean> = {};
|
|
const allOriginalTags: NameValue[] = [];
|
|
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return allOriginalTags;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.originalTagNames) {
|
|
continue;
|
|
}
|
|
|
|
for (const tagName of importTransaction.originalTagNames) {
|
|
allOriginalTagNames[tagName] = true;
|
|
}
|
|
}
|
|
|
|
for (const name of keys(allOriginalTagNames)) {
|
|
allOriginalTags.push({
|
|
name: name || tt('(Empty)'),
|
|
value: name
|
|
});
|
|
}
|
|
|
|
return allOriginalTags;
|
|
}
|
|
|
|
function importTransactionsFilter(value: string, query: string, item?: { value: unknown, raw: ImportTransaction }): boolean {
|
|
if (!item || !item.raw) {
|
|
return false;
|
|
}
|
|
|
|
return isTransactionDisplayed(item.raw);
|
|
}
|
|
|
|
function selectAllValid(): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.valid && isTransactionDisplayed(importTransaction)) {
|
|
importTransaction.selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectAllInvalid(): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.valid && isTransactionDisplayed(importTransaction)) {
|
|
importTransaction.selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectAll(): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (isTransactionDisplayed(importTransaction)) {
|
|
importTransaction.selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectNone(): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (isTransactionDisplayed(importTransaction)) {
|
|
importTransaction.selected = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectInvert(): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (isTransactionDisplayed(importTransaction)) {
|
|
importTransaction.selected = !importTransaction.selected;
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectAllInThisPage(): void {
|
|
for (const importTransaction of currentPageTransactions.value) {
|
|
importTransaction.selected = true;
|
|
}
|
|
}
|
|
|
|
function selectNoneInThisPage(): void {
|
|
for (const importTransaction of currentPageTransactions.value) {
|
|
importTransaction.selected = false;
|
|
}
|
|
}
|
|
|
|
function selectInvertInThisPage(): void {
|
|
for (const importTransaction of currentPageTransactions.value) {
|
|
importTransaction.selected = !importTransaction.selected;
|
|
}
|
|
}
|
|
|
|
function editTransaction(transaction: ImportTransaction): void {
|
|
if (editingTransaction.value) {
|
|
editingTransaction.value.tagIds = editingTags.value;
|
|
updateTransactionData(editingTransaction.value);
|
|
}
|
|
|
|
if (editingTransaction.value === transaction) {
|
|
editingTags.value = [];
|
|
editingTransaction.value = null;
|
|
} else {
|
|
editingTransaction.value = transaction;
|
|
editingTags.value = editingTransaction.value.tagIds;
|
|
}
|
|
}
|
|
|
|
function updateTransactionData(transaction: ImportTransaction): void {
|
|
transaction.valid = transaction.isTransactionValid();
|
|
|
|
if (transaction.categoryId && allCategoriesMap.value[transaction.categoryId]) {
|
|
transaction.actualCategoryName = allCategoriesMap.value[transaction.categoryId].name;
|
|
}
|
|
|
|
if (transaction.sourceAccountId && allAccountsMap.value[transaction.sourceAccountId]) {
|
|
transaction.actualSourceAccountName = allAccountsMap.value[transaction.sourceAccountId].name;
|
|
}
|
|
|
|
if (transaction.destinationAccountId && allAccountsMap.value[transaction.destinationAccountId]) {
|
|
transaction.actualDestinationAccountName = allAccountsMap.value[transaction.destinationAccountId].name;
|
|
}
|
|
}
|
|
|
|
function showBatchReplaceDialog(type: BatchReplaceDialogDataType, allSourceTagItems?: NameValue[]): void {
|
|
if (isEditing.value) {
|
|
return;
|
|
}
|
|
|
|
batchReplaceDialog.value?.open({
|
|
mode: 'batchReplace',
|
|
type: type,
|
|
allSourceTagItems: allSourceTagItems
|
|
}).then(result => {
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
if (type !== 'tag') {
|
|
if (!result.targetItem) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let updatedCount = 0;
|
|
|
|
if (props.importTransactions) {
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.selected) {
|
|
continue;
|
|
}
|
|
|
|
let updated = false;
|
|
|
|
if (type === 'expenseCategory') {
|
|
if (importTransaction.type === TransactionType.Expense) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
} else if (type === 'incomeCategory') {
|
|
if (importTransaction.type === TransactionType.Income) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
} else if (type === 'transferCategory') {
|
|
if (importTransaction.type === TransactionType.Transfer) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
} else if (type === 'account') {
|
|
importTransaction.sourceAccountId = result.targetItem as string;
|
|
updated = true;
|
|
} else if (type === 'destinationAccount') {
|
|
if (importTransaction.type === TransactionType.Transfer) {
|
|
importTransaction.destinationAccountId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
} else if (type === 'tag') {
|
|
let removeIndex: number[] = [];
|
|
|
|
for (let tagIndex = 0; tagIndex < importTransaction.originalTagNames.length; tagIndex++) {
|
|
const originalTagName = importTransaction.originalTagNames ? (importTransaction.originalTagNames[tagIndex] ?? '') : '';
|
|
|
|
if (originalTagName === result.sourceItem) {
|
|
if (result.targetItem) {
|
|
importTransaction.tagIds[tagIndex] = result.targetItem;
|
|
importTransaction.originalTagNames[tagIndex] = allTagsMap.value[result.targetItem]?.name || '';
|
|
} else {
|
|
removeIndex.push(tagIndex);
|
|
}
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
for (const tagIndex of reversed(removeIndex)) {
|
|
importTransaction.tagIds.splice(tagIndex, 1);
|
|
importTransaction.originalTagNames.splice(tagIndex, 1);
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
updatedCount++;
|
|
updateTransactionData(importTransaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updatedCount > 0) {
|
|
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
|
|
count: getDisplayCount(updatedCount)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function showReplaceInvalidItemDialog(type: BatchReplaceDialogDataType, invalidItems: NameValue[]): void {
|
|
if (isEditing.value) {
|
|
return;
|
|
}
|
|
|
|
batchReplaceDialog.value?.open({
|
|
mode: 'replaceInvalidItems',
|
|
type: type,
|
|
invalidItems: invalidItems
|
|
}).then(result => {
|
|
if (!result || (!result.sourceItem && result.sourceItem !== '')) {
|
|
return;
|
|
}
|
|
|
|
if (type !== 'tag') {
|
|
if (!result.targetItem) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let updatedCount = 0;
|
|
|
|
if (props.importTransactions) {
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.valid) {
|
|
continue;
|
|
}
|
|
|
|
let updated = false;
|
|
|
|
if (type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory') {
|
|
const categoryId = importTransaction.categoryId;
|
|
const originalCategoryName = importTransaction.originalCategoryName;
|
|
|
|
if (importTransaction.type !== TransactionType.ModifyBalance && originalCategoryName === result.sourceItem && (!categoryId || categoryId === '0' || !allCategoriesMap.value[categoryId])) {
|
|
if (type === 'expenseCategory' && importTransaction.type === TransactionType.Expense) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
} else if (type === 'incomeCategory' && importTransaction.type === TransactionType.Income) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
} else if (type === 'transferCategory' && importTransaction.type === TransactionType.Transfer) {
|
|
importTransaction.categoryId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
}
|
|
} else if (type === 'account') {
|
|
const sourceAccountId = importTransaction.sourceAccountId;
|
|
const originalSourceAccountName = importTransaction.originalSourceAccountName;
|
|
const destinationAccountId = importTransaction.destinationAccountId;
|
|
const originalDestinationAccountName = importTransaction.originalDestinationAccountName;
|
|
|
|
if (originalSourceAccountName === result.sourceItem && (!sourceAccountId || sourceAccountId === '0' || !allAccountsMap.value[sourceAccountId])) {
|
|
importTransaction.sourceAccountId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
|
|
if (importTransaction.type === TransactionType.Transfer && originalDestinationAccountName === result.sourceItem && (!destinationAccountId || destinationAccountId === '0' || !allAccountsMap.value[destinationAccountId])) {
|
|
importTransaction.destinationAccountId = result.targetItem as string;
|
|
updated = true;
|
|
}
|
|
} else if (type === 'tag' && importTransaction.tagIds) {
|
|
let removeIndex: number[] = [];
|
|
|
|
for (let tagIndex = 0; tagIndex < importTransaction.tagIds.length; tagIndex++) {
|
|
const tagId = importTransaction.tagIds[tagIndex] as string;
|
|
const originalTagName = importTransaction.originalTagNames ? (importTransaction.originalTagNames[tagIndex] ?? '') : '';
|
|
|
|
if (originalTagName === result.sourceItem && (!tagId || tagId === '0' || !allTagsMap.value[tagId])) {
|
|
if (result.targetItem) {
|
|
importTransaction.tagIds[tagIndex] = result.targetItem;
|
|
importTransaction.originalTagNames[tagIndex] = allTagsMap.value[result.targetItem]?.name || '';
|
|
} else {
|
|
removeIndex.push(tagIndex);
|
|
}
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
for (const tagIndex of reversed(removeIndex)) {
|
|
importTransaction.tagIds.splice(tagIndex, 1);
|
|
importTransaction.originalTagNames.splice(tagIndex, 1);
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
updatedCount++;
|
|
updateTransactionData(importTransaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updatedCount > 0) {
|
|
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
|
|
count: getDisplayCount(updatedCount)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function showReplaceAllTypesDialog(): void {
|
|
if (isEditing.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 (props.importTransactions) {
|
|
for (const importTransaction of props.importTransactions) {
|
|
let updated = false;
|
|
|
|
for (const rule of result.rules) {
|
|
if (!rule || !rule.dataType || !rule.sourceValue || !rule.targetId) {
|
|
continue;
|
|
}
|
|
|
|
if (rule.dataType === 'expenseCategory' || rule.dataType === 'incomeCategory' || rule.dataType === 'transferCategory') {
|
|
if (importTransaction.type !== TransactionType.ModifyBalance && importTransaction.originalCategoryName === rule.sourceValue) {
|
|
importTransaction.categoryId = rule.targetId;
|
|
updated = true;
|
|
}
|
|
} else if (rule.dataType === 'account') {
|
|
if (importTransaction.originalSourceAccountName === rule.sourceValue) {
|
|
importTransaction.sourceAccountId = rule.targetId;
|
|
updated = true;
|
|
}
|
|
|
|
if (importTransaction.type === TransactionType.Transfer && importTransaction.originalDestinationAccountName === rule.sourceValue) {
|
|
importTransaction.destinationAccountId = rule.targetId;
|
|
updated = true;
|
|
}
|
|
} else if (rule.dataType === 'tag' && importTransaction.tagIds) {
|
|
for (let tagIndex = 0; tagIndex < importTransaction.tagIds.length; tagIndex++) {
|
|
const originalTagName = importTransaction.originalTagNames ? (importTransaction.originalTagNames[tagIndex] ?? '') : '';
|
|
|
|
if (originalTagName === rule.sourceValue) {
|
|
importTransaction.tagIds[tagIndex] = rule.targetId;
|
|
updated = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
updatedCount++;
|
|
updateTransactionData(importTransaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updatedCount > 0) {
|
|
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
|
|
count: getDisplayCount(updatedCount)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function showBatchCreateInvalidItemDialog(type: BatchCreateDialogDataType, invalidItems: NameValue[]): void {
|
|
if (isEditing.value) {
|
|
return;
|
|
}
|
|
|
|
batchCreateDialog.value?.open({
|
|
type: type,
|
|
invalidItems: invalidItems
|
|
}).then(result => {
|
|
if (!result || !result.sourceTargetMap) {
|
|
return;
|
|
}
|
|
|
|
let updatedCount = 0;
|
|
|
|
if (props.importTransactions) {
|
|
const sourceTargetMap: Record<string, string> = result.sourceTargetMap;
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (importTransaction.valid) {
|
|
continue;
|
|
}
|
|
|
|
let updated = false;
|
|
|
|
if (type === 'expenseCategory' || type === 'incomeCategory' || type === 'transferCategory') {
|
|
const categoryId = importTransaction.categoryId;
|
|
const originalCategoryName = importTransaction.originalCategoryName;
|
|
const targetItem = sourceTargetMap[originalCategoryName];
|
|
|
|
if (importTransaction.type !== TransactionType.ModifyBalance && targetItem && (!categoryId || categoryId === '0' || !allCategoriesMap.value[categoryId])) {
|
|
if (type === 'expenseCategory' && importTransaction.type === TransactionType.Expense) {
|
|
importTransaction.categoryId = targetItem;
|
|
updated = true;
|
|
} else if (type === 'incomeCategory' && importTransaction.type === TransactionType.Income) {
|
|
importTransaction.categoryId = targetItem;
|
|
updated = true;
|
|
} else if (type === 'transferCategory' && importTransaction.type === TransactionType.Transfer) {
|
|
importTransaction.categoryId = targetItem;
|
|
updated = true;
|
|
}
|
|
}
|
|
} else if (type === 'tag' && importTransaction.tagIds) {
|
|
for (let tagIndex = 0; tagIndex < importTransaction.tagIds.length; tagIndex++) {
|
|
const tagId = importTransaction.tagIds[tagIndex] as string;
|
|
const originalTagName = importTransaction.originalTagNames ? (importTransaction.originalTagNames[tagIndex] ?? '') : '';
|
|
const targetItem = sourceTargetMap[originalTagName];
|
|
|
|
if (targetItem && (!tagId || tagId === '0' || !allTagsMap.value[tagId])) {
|
|
importTransaction.tagIds[tagIndex] = targetItem;
|
|
updated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
updatedCount++;
|
|
updateTransactionData(importTransaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updatedCount > 0) {
|
|
snackbar.value?.showMessage('format.misc.youHaveUpdatedTransactions', {
|
|
count: getDisplayCount(updatedCount)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function convertTransactionType(fromType: TransactionType, toType: TransactionType): void {
|
|
if (!props.importTransactions || props.importTransactions.length < 1) {
|
|
return;
|
|
}
|
|
|
|
const categoryType = transactionTypeToCategoryType(toType);
|
|
|
|
if (!categoryType) {
|
|
return;
|
|
}
|
|
|
|
const categoryMapByName: Record<string, TransactionCategory> = getSecondaryTransactionMapByName(allCategories.value[categoryType]);
|
|
|
|
for (const importTransaction of props.importTransactions) {
|
|
if (!importTransaction.selected || importTransaction.type !== fromType) {
|
|
continue;
|
|
}
|
|
|
|
importTransaction.type = toType;
|
|
importTransaction.categoryId = categoryMapByName[importTransaction.originalCategoryName]?.id || '0';
|
|
|
|
if (importTransaction.type === TransactionType.Transfer) {
|
|
importTransaction.destinationAccountId = allAccountsMapByName.value[importTransaction.originalDestinationAccountName || '']?.id || '0';
|
|
importTransaction.destinationAmount = importTransaction.sourceAmount;
|
|
} else {
|
|
if (fromType === TransactionType.Transfer && toType === TransactionType.Income) {
|
|
importTransaction.sourceAccountId = importTransaction.destinationAccountId;
|
|
importTransaction.sourceAmount = importTransaction.destinationAmount;
|
|
}
|
|
|
|
importTransaction.destinationAccountId = '0';
|
|
importTransaction.destinationAmount = 0;
|
|
}
|
|
|
|
updateTransactionData(importTransaction);
|
|
}
|
|
}
|
|
|
|
function changeCustomDateFilter(minTime: number, maxTime: number): void {
|
|
filters.value.minDatetime = minTime;
|
|
filters.value.maxDatetime = maxTime;
|
|
showCustomDateRangeDialog.value = false;
|
|
}
|
|
|
|
function onShowDateRangeError(message: string): void {
|
|
snackbar.value?.showError(message);
|
|
}
|
|
|
|
function reset(): void {
|
|
editingTransaction.value = null;
|
|
editingTags.value = [];
|
|
filters.value.minDatetime = null;
|
|
filters.value.maxDatetime = null;
|
|
filters.value.transactionType = null;
|
|
filters.value.category = null;
|
|
filters.value.account = null;
|
|
filters.value.tag = null;
|
|
filters.value.description = null;
|
|
currentPage.value = 1;
|
|
countPerPage.value = 10;
|
|
}
|
|
|
|
function setCountPerPage(count: number): void {
|
|
countPerPage.value = count;
|
|
}
|
|
|
|
defineExpose({
|
|
filterMenus,
|
|
toolMenus,
|
|
isEditing,
|
|
canImport,
|
|
reset,
|
|
setCountPerPage
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.import-transaction-table .v-autocomplete.v-input.v-input--density-compact:not(.v-textarea) .v-field__input,
|
|
.import-transaction-table .v-select.v-input.v-input--density-compact:not(.v-textarea) .v-field__input {
|
|
min-height: inherit;
|
|
padding-top: 4px;
|
|
}
|
|
|
|
.import-transaction-table .v-chip.transaction-tag {
|
|
margin-inline-end: 4px;
|
|
margin-top: 2px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.import-transaction-table .v-chip.transaction-tag > .v-chip__content {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
</style>
|