support setting account/transaction category filter for statistics page
This commit is contained in:
@@ -11,6 +11,7 @@ import { VBtn } from 'vuetify/components/VBtn';
|
||||
import { VBtnGroup } from 'vuetify/components/VBtnGroup';
|
||||
import { VBtnToggle } from 'vuetify/components/VBtnToggle';
|
||||
import { VCard, VCardActions, VCardItem, VCardSubtitle, VCardText, VCardTitle } from 'vuetify/components/VCard';
|
||||
import { VCheckbox, VCheckboxBtn } from 'vuetify/components/VCheckbox';
|
||||
import { VChip } from 'vuetify/components/VChip';
|
||||
import { VDialog } from 'vuetify/components/VDialog';
|
||||
import { VDivider } from 'vuetify/components/VDivider';
|
||||
@@ -109,6 +110,8 @@ const vuetify = createVuetify({
|
||||
VCardSubtitle,
|
||||
VCardText,
|
||||
VCardTitle,
|
||||
VCheckbox,
|
||||
VCheckboxBtn,
|
||||
VChip,
|
||||
VDialog,
|
||||
VDivider,
|
||||
|
||||
@@ -62,15 +62,11 @@
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card :title="$t('Default Account Filter')">
|
||||
|
||||
</v-card>
|
||||
<account-filter-settings-card :auto-save="true" :modify-default="true" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card :title="$t('Default Transaction Category Filter')">
|
||||
|
||||
</v-card>
|
||||
<category-filter-settings-card :auto-save="true" :modify-default="true" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
@@ -81,7 +77,14 @@ import { useSettingsStore } from '@/stores/setting.js';
|
||||
|
||||
import statisticsConstants from '@/consts/statistics.js';
|
||||
|
||||
import AccountFilterSettingsCard from '@/views/desktop/statistics/AccountFilterSettingsCard.vue';
|
||||
import CategoryFilterSettingsCard from '@/views/desktop/statistics/CategoryFilterSettingsCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountFilterSettingsCard,
|
||||
CategoryFilterSettingsCard
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore),
|
||||
allChartTypes() {
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" v-if="dialogMode">
|
||||
<v-toolbar-title>{{ $t('Default Account Filter') }}</v-toolbar-title>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:disabled="loading" :icon="true">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.selectAll"
|
||||
:title="$t('Select All')"
|
||||
@click="selectAll"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectNone"
|
||||
:title="$t('Select None')"
|
||||
@click="selectNone"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectInverse"
|
||||
:title="$t('Invert Selection')"
|
||||
@click="selectInvert"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<template #title v-if="!dialogMode">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ $t('Default Account Filter') }}</span>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:disabled="loading" :icon="true">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.selectAll"
|
||||
:title="$t('Select All')"
|
||||
@click="selectAll"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectNone"
|
||||
:title="$t('Select None')"
|
||||
@click="selectNone"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectInverse"
|
||||
:title="$t('Invert Selection')"
|
||||
@click="selectInvert"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card-text v-if="loading">
|
||||
<v-skeleton-loader type="paragraph" :loading="loading"
|
||||
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]"></v-skeleton-loader>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-if="!loading && !hasAnyAvailableAccount">
|
||||
<span class="text-subtitle-1">{{ $t('No available account') }}</span>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else-if="!loading && hasAnyAvailableAccount">
|
||||
<v-expansion-panels class="account-categories" multiple v-model="expandAccountCategories">
|
||||
<v-expansion-panel :key="accountCategory.category"
|
||||
:value="accountCategory.category"
|
||||
class="border"
|
||||
v-for="accountCategory in allVisibleCategorizedAccounts">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ml-3">{{ $t(accountCategory.name) }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<template :key="account.id"
|
||||
v-for="(account, idx) in accountCategory.visibleAccounts">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-checkbox :model-value="isAccountOrSubAccountsAllChecked(account, filterAccountIds)"
|
||||
:indeterminate="isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds)"
|
||||
@update:model-value="selectAccountOrSubAccounts(account, $event)">
|
||||
<template #label>
|
||||
<ItemIcon class="d-flex" icon-type="account"
|
||||
:icon-id="account.icon" :color="account.color"></ItemIcon>
|
||||
<span class="ml-3">{{ account.name }}</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="account.type === allAccountTypes.MultiSubAccounts && accountCategory.visibleSubAccounts[account.id]"/>
|
||||
|
||||
<v-list rounded density="comfortable" class="pa-0 ml-4"
|
||||
v-if="account.type === allAccountTypes.MultiSubAccounts && accountCategory.visibleSubAccounts[account.id]">
|
||||
<template :key="subAccount.id"
|
||||
v-for="(subAccount, subIdx) in accountCategory.visibleSubAccounts[account.id]">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-checkbox :model-value="isAccountChecked(subAccount, filterAccountIds)"
|
||||
@update:model-value="selectAccount(subAccount, $event)">
|
||||
<template #label>
|
||||
<ItemIcon class="d-flex" icon-type="account"
|
||||
:icon-id="subAccount.icon" :color="subAccount.color"></ItemIcon>
|
||||
<span class="ml-3">{{ subAccount.name }}</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="subIdx !== accountCategory.visibleSubAccounts[account.id].length - 1"/>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="idx !== accountCategory.visibleAccounts.length - 1"/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="mt-3" v-if="dialogMode">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="save">{{ $t('OK') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapStores } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/setting.js';
|
||||
import { useAccountsStore } from '@/stores/account.js';
|
||||
import { useStatisticsStore } from '@/stores/statistics.js';
|
||||
|
||||
import accountConstants from '@/consts/account.js';
|
||||
import { copyObjectTo } from '@/lib/common.js';
|
||||
import { getVisibleCategorizedAccounts } from '@/lib/account.js';
|
||||
|
||||
import {
|
||||
mdiSelectAll,
|
||||
mdiSelect,
|
||||
mdiSelectInverse,
|
||||
mdiDotsVertical
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'dialogMode',
|
||||
'modifyDefault',
|
||||
'autoSave'
|
||||
],
|
||||
emits: [
|
||||
'settings:change'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
loading: true,
|
||||
expandAccountCategories: accountConstants.allCategories.map(category => category.id),
|
||||
filterAccountIds: {},
|
||||
icons: {
|
||||
selectAll: mdiSelectAll,
|
||||
selectNone: mdiSelect,
|
||||
selectInverse: mdiSelectInverse,
|
||||
more: mdiDotsVertical
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useAccountsStore, useStatisticsStore),
|
||||
title() {
|
||||
if (this.modifyDefault) {
|
||||
return 'Default Account Filter';
|
||||
} else {
|
||||
return 'Filter Accounts';
|
||||
}
|
||||
},
|
||||
applyText() {
|
||||
if (this.modifyDefault) {
|
||||
return 'Save';
|
||||
} else {
|
||||
return 'Apply';
|
||||
}
|
||||
},
|
||||
allAccountTypes() {
|
||||
return accountConstants.allAccountTypes;
|
||||
},
|
||||
allVisibleCategorizedAccounts() {
|
||||
return getVisibleCategorizedAccounts(this.accountsStore.allCategorizedAccounts);
|
||||
},
|
||||
hasAnyAvailableAccount() {
|
||||
return this.accountsStore.allVisibleAccountsCount > 0;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
|
||||
self.accountsStore.loadAllAccounts({
|
||||
force: false
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
|
||||
const allAccountIds = {};
|
||||
|
||||
for (let accountId in self.accountsStore.allAccountsMap) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.accountsStore.allAccountsMap, accountId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const account = self.accountsStore.allAccountsMap[accountId];
|
||||
allAccountIds[account.id] = false;
|
||||
}
|
||||
|
||||
if (self.modifyDefault) {
|
||||
self.filterAccountIds = copyObjectTo(self.settingsStore.appSettings.statistics.defaultAccountFilter, allAccountIds);
|
||||
} else {
|
||||
self.filterAccountIds = copyObjectTo(self.statisticsStore.transactionStatisticsFilter.filterAccountIds, allAccountIds);
|
||||
}
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const self = this;
|
||||
|
||||
const filteredAccountIds = {};
|
||||
|
||||
for (let accountId in self.filterAccountIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.filterAccountIds, accountId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self.filterAccountIds[accountId]) {
|
||||
filteredAccountIds[accountId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.modifyDefault) {
|
||||
self.settingsStore.setStatisticsDefaultAccountFilter(filteredAccountIds);
|
||||
} else {
|
||||
self.statisticsStore.updateTransactionStatisticsFilter({
|
||||
filterAccountIds: filteredAccountIds
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('settings:change', true);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('settings:change', false);
|
||||
},
|
||||
selectAccountOrSubAccounts(account, value) {
|
||||
if (account.type === this.allAccountTypes.SingleAccount) {
|
||||
this.filterAccountIds[account.id] = !value;
|
||||
} else if (account.type === this.allAccountTypes.MultiSubAccounts) {
|
||||
if (!account.subAccounts || !account.subAccounts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < account.subAccounts.length; i++) {
|
||||
const subAccount = account.subAccounts[i];
|
||||
this.filterAccountIds[subAccount.id] = !value;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectAccount(account, value) {
|
||||
this.filterAccountIds[account.id] = !value;
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
for (let accountId in this.filterAccountIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterAccountIds, accountId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const account = this.accountsStore.allAccountsMap[accountId];
|
||||
|
||||
if (account && account.type === this.allAccountTypes.SingleAccount) {
|
||||
this.filterAccountIds[account.id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectNone() {
|
||||
for (let accountId in this.filterAccountIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterAccountIds, accountId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const account = this.accountsStore.allAccountsMap[accountId];
|
||||
|
||||
if (account && account.type === this.allAccountTypes.SingleAccount) {
|
||||
this.filterAccountIds[account.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectInvert() {
|
||||
for (let accountId in this.filterAccountIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterAccountIds, accountId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const account = this.accountsStore.allAccountsMap[accountId];
|
||||
|
||||
if (account && account.type === this.allAccountTypes.SingleAccount) {
|
||||
this.filterAccountIds[account.id] = !this.filterAccountIds[account.id];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
isAccountChecked(account, filterAccountIds) {
|
||||
return !filterAccountIds[account.id];
|
||||
},
|
||||
isAccountOrSubAccountsAllChecked(account, filterAccountIds) {
|
||||
if (!account.subAccounts) {
|
||||
return !filterAccountIds[account.id];
|
||||
}
|
||||
|
||||
for (let i = 0; i < account.subAccounts.length; i++) {
|
||||
const subAccount = account.subAccounts[i];
|
||||
if (filterAccountIds[subAccount.id]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds) {
|
||||
if (!account.subAccounts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let checkedCount = 0;
|
||||
|
||||
for (let i = 0; i < account.subAccounts.length; i++) {
|
||||
const subAccount = account.subAccounts[i];
|
||||
if (!filterAccountIds[subAccount.id]) {
|
||||
checkedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return checkedCount > 0 && checkedCount < account.subAccounts.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.account-categories .v-expansion-panel-text__wrapper {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" v-if="dialogMode">
|
||||
<v-toolbar-title>{{ $t('Default Transaction Category Filter') }}</v-toolbar-title>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:disabled="loading" :icon="true">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.selectAll"
|
||||
:title="$t('Select All')"
|
||||
@click="selectAll"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectNone"
|
||||
:title="$t('Select None')"
|
||||
@click="selectNone"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectInverse"
|
||||
:title="$t('Invert Selection')"
|
||||
@click="selectInvert"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<template #title v-if="!dialogMode">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ $t('Default Transaction Category Filter') }}</span>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:disabled="loading" :icon="true">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.selectAll"
|
||||
:title="$t('Select All')"
|
||||
@click="selectAll"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectNone"
|
||||
:title="$t('Select None')"
|
||||
@click="selectNone"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.selectInverse"
|
||||
:title="$t('Invert Selection')"
|
||||
@click="selectInvert"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card-text v-if="loading">
|
||||
<v-skeleton-loader type="paragraph" :loading="loading"
|
||||
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]"></v-skeleton-loader>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else-if="!loading">
|
||||
<v-expansion-panels class="category-types" multiple v-model="expandCategoryTypes">
|
||||
<v-expansion-panel :key="transactionType.type"
|
||||
:value="transactionType.type"
|
||||
class="border"
|
||||
v-for="transactionType in allVisibleTransactionCategories">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ml-3">{{ getCategoryTypeName(transactionType.type) }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<div class="py-3" v-if="!hasAvailableCategory[transactionType.type]">{{ $t('No available category') }}</div>
|
||||
|
||||
<template :key="category.id"
|
||||
v-for="(category, idx) in transactionType.visibleCategories">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-checkbox :model-value="isSubCategoriesAllChecked(category, filterCategoryIds)"
|
||||
:indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
|
||||
@update:model-value="selectSubCategories(category, $event)">
|
||||
<template #label>
|
||||
<ItemIcon class="d-flex" icon-type="category"
|
||||
:icon-id="category.icon" :color="category.color"></ItemIcon>
|
||||
<span class="ml-3">{{ category.name }}</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="transactionType.visibleSubCategories[category.id]"/>
|
||||
|
||||
<v-list rounded density="comfortable" class="pa-0 ml-4"
|
||||
v-if="transactionType.visibleSubCategories[category.id]">
|
||||
<template :key="subCategory.id"
|
||||
v-for="(subCategory, subIdx) in transactionType.visibleSubCategories[category.id]">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-checkbox :model-value="isCategoryChecked(subCategory, filterCategoryIds)"
|
||||
@update:model-value="selectCategory(subCategory, $event)">
|
||||
<template #label>
|
||||
<ItemIcon class="d-flex" icon-type="category"
|
||||
:icon-id="subCategory.icon" :color="subCategory.color"></ItemIcon>
|
||||
<span class="ml-3">{{ subCategory.name }}</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="subIdx !== transactionType.visibleSubCategories[category.id].length - 1"/>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="idx !== transactionType.visibleCategories - 1"/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="mt-3" v-if="dialogMode">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="save">{{ $t('OK') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapStores } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/setting.js';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js';
|
||||
import { useStatisticsStore } from '@/stores/statistics.js';
|
||||
|
||||
import categoryConstants from '@/consts/category.js';
|
||||
import { copyObjectTo } from '@/lib/common.js';
|
||||
import { allVisibleTransactionCategories } from '@/lib/category.js';
|
||||
|
||||
import {
|
||||
mdiSelectAll,
|
||||
mdiSelect,
|
||||
mdiSelectInverse,
|
||||
mdiDotsVertical
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'dialogMode',
|
||||
'modifyDefault',
|
||||
'autoSave'
|
||||
],
|
||||
emits: [
|
||||
'settings:change'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
loading: true,
|
||||
expandCategoryTypes: [
|
||||
categoryConstants.allCategoryTypes.Income.toString(),
|
||||
categoryConstants.allCategoryTypes.Expense.toString(),
|
||||
categoryConstants.allCategoryTypes.Transfer.toString()
|
||||
],
|
||||
filterCategoryIds: {},
|
||||
icons: {
|
||||
selectAll: mdiSelectAll,
|
||||
selectNone: mdiSelect,
|
||||
selectInverse: mdiSelectInverse,
|
||||
more: mdiDotsVertical
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useTransactionCategoriesStore, useStatisticsStore),
|
||||
title() {
|
||||
if (this.modifyDefault) {
|
||||
return 'Default Transaction Category Filter';
|
||||
} else {
|
||||
return 'Filter Transaction Categories';
|
||||
}
|
||||
},
|
||||
applyText() {
|
||||
if (this.modifyDefault) {
|
||||
return 'Save';
|
||||
} else {
|
||||
return 'Apply';
|
||||
}
|
||||
},
|
||||
allVisibleTransactionCategories() {
|
||||
return allVisibleTransactionCategories(this.transactionCategoriesStore.allTransactionCategories);
|
||||
},
|
||||
hasAnyAvailableCategory() {
|
||||
for (let type in this.allVisibleTransactionCategories) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.allVisibleTransactionCategories, type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryType = this.allVisibleTransactionCategories[type];
|
||||
|
||||
if (categoryType.visibleCategories && categoryType.visibleCategories.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
hasAvailableCategory() {
|
||||
const result = {};
|
||||
|
||||
for (let type in this.allVisibleTransactionCategories) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.allVisibleTransactionCategories, type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryType = this.allVisibleTransactionCategories[type];
|
||||
result[type] = categoryType.visibleCategories && categoryType.visibleCategories.length > 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
|
||||
self.transactionCategoriesStore.loadAllCategories({
|
||||
force: false
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
|
||||
const allCategoryIds = {};
|
||||
|
||||
for (let categoryId in self.transactionCategoriesStore.allTransactionCategoriesMap) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.transactionCategoriesStore.allTransactionCategoriesMap, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = self.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
allCategoryIds[category.id] = false;
|
||||
}
|
||||
|
||||
if (self.modifyDefault) {
|
||||
self.filterCategoryIds = copyObjectTo(self.settingsStore.appSettings.statistics.defaultTransactionCategoryFilter, allCategoryIds);
|
||||
} else {
|
||||
self.filterCategoryIds = copyObjectTo(self.statisticsStore.transactionStatisticsFilter.filterCategoryIds, allCategoryIds);
|
||||
}
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const self = this;
|
||||
|
||||
const filteredCategoryIds = {};
|
||||
|
||||
for (let categoryId in self.filterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.filterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self.filterCategoryIds[categoryId]) {
|
||||
filteredCategoryIds[categoryId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.modifyDefault) {
|
||||
self.settingsStore.setStatisticsDefaultTransactionCategoryFilter(filteredCategoryIds);
|
||||
} else {
|
||||
self.statisticsStore.updateTransactionStatisticsFilter({
|
||||
filterCategoryIds: filteredCategoryIds
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('settings:change', true);
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('settings:change', false);
|
||||
},
|
||||
selectCategory(category, value) {
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterCategoryIds[category.id] = !value;
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectSubCategories(category, value) {
|
||||
if (!category || !category.subCategories || !category.subCategories.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
const subCategory = category.subCategories[i];
|
||||
this.filterCategoryIds[subCategory.id] = !value;
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
for (let categoryId in this.filterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = this.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (category) {
|
||||
this.filterCategoryIds[category.id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectNone() {
|
||||
for (let categoryId in this.filterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = this.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (category) {
|
||||
this.filterCategoryIds[category.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
selectInvert() {
|
||||
for (let categoryId in this.filterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.filterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = this.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (category) {
|
||||
this.filterCategoryIds[category.id] = !this.filterCategoryIds[category.id];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoSave) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
getCategoryTypeName(categoryType) {
|
||||
switch (categoryType) {
|
||||
case categoryConstants.allCategoryTypes.Income.toString():
|
||||
return this.$t('Income Categories');
|
||||
case categoryConstants.allCategoryTypes.Expense.toString():
|
||||
return this.$t('Expense Categories');
|
||||
case categoryConstants.allCategoryTypes.Transfer.toString():
|
||||
return this.$t('Transfer Categories');
|
||||
default:
|
||||
return this.$t('Transaction Categories');
|
||||
}
|
||||
},
|
||||
isCategoryChecked(category, filterCategoryIds) {
|
||||
return !filterCategoryIds[category.id];
|
||||
},
|
||||
isSubCategoriesAllChecked(category, filterCategoryIds) {
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
const subCategory = category.subCategories[i];
|
||||
if (filterCategoryIds[subCategory.id]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
isSubCategoriesHasButNotAllChecked(category, filterCategoryIds) {
|
||||
let checkedCount = 0;
|
||||
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
const subCategory = category.subCategories[i];
|
||||
if (!filterCategoryIds[subCategory.id]) {
|
||||
checkedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return checkedCount > 0 && checkedCount < category.subCategories.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.category-types .v-expansion-panel-text__wrapper {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,10 +105,10 @@
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.filter"
|
||||
:title="$t('Filter Accounts')"
|
||||
@click="filterAccounts"></v-list-item>
|
||||
@click="showFilterAccountDialog = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.filter"
|
||||
:title="$t('Filter Transaction Categories')"
|
||||
@click="filterCategories"></v-list-item>
|
||||
@click="showFilterCategoryDialog = true"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="icons.filterSettings"
|
||||
:title="$t('Settings')"
|
||||
@@ -199,6 +199,19 @@
|
||||
:max-time="query.endTime"
|
||||
v-model:show="showCustomDateRangeDialog"
|
||||
@dateRange:change="setCustomDateFilter" />
|
||||
|
||||
<v-dialog scrollable max-width="600" max-height="600" v-model="showFilterAccountDialog">
|
||||
<account-filter-settings-card
|
||||
:dialog-mode="true" :modify-default="false"
|
||||
@settings:change="showFilterAccountDialog = false" />
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog scrollable max-width="600" max-height="600" v-model="showFilterCategoryDialog">
|
||||
<category-filter-settings-card
|
||||
:dialog-mode="true" :modify-default="false"
|
||||
@settings:change="showFilterCategoryDialog = false" />
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
@@ -234,13 +247,22 @@ import {
|
||||
mdiDotsVertical,
|
||||
} from '@mdi/js';
|
||||
|
||||
import AccountFilterSettingsCard from '@/views/desktop/statistics/AccountFilterSettingsCard.vue';
|
||||
import CategoryFilterSettingsCard from '@/views/desktop/statistics/CategoryFilterSettingsCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountFilterSettingsCard,
|
||||
CategoryFilterSettingsCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'statisticsPage',
|
||||
initing: true,
|
||||
loading: true,
|
||||
showCustomDateRangeDialog: false,
|
||||
showFilterAccountDialog: false,
|
||||
showFilterCategoryDialog: false,
|
||||
icons: {
|
||||
check: mdiCheck,
|
||||
left: mdiArrowLeft,
|
||||
@@ -576,12 +598,6 @@ export default {
|
||||
},
|
||||
clickPieChartItem(item) {
|
||||
this.$router.push(this.getItemLinkUrl(item));
|
||||
},
|
||||
filterAccounts() {
|
||||
|
||||
},
|
||||
filterCategories() {
|
||||
|
||||
},
|
||||
settings() {
|
||||
this.$router.push('/app/settings?tab=statisticsSetting');
|
||||
|
||||
Reference in New Issue
Block a user