mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 07:57:33 +08:00
migrate category filter page to composition API and typescript
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn">
|
||||
<f7-navbar>
|
||||
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
|
||||
<f7-nav-title :title="$t(title)"></f7-nav-title>
|
||||
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
|
||||
<f7-nav-title :title="tt(title)"></f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link icon-f7="ellipsis" :class="{ 'disabled': !hasAnyAvailableCategory }" @click="showMoreActionSheet = true"></f7-link>
|
||||
<f7-link :text="$t(applyText)" :class="{ 'disabled': !hasAnyVisibleCategory }" @click="save"></f7-link>
|
||||
<f7-link :text="tt(applyText)" :class="{ 'disabled': !hasAnyVisibleCategory }" @click="save"></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
@@ -51,41 +51,41 @@
|
||||
</div>
|
||||
|
||||
<f7-block class="combination-list-wrapper margin-vertical"
|
||||
:key="transactionType.type"
|
||||
v-for="transactionType in allTransactionCategories"
|
||||
:key="categoryType.type"
|
||||
v-for="categoryType in allTransactionCategories"
|
||||
v-else-if="!loading">
|
||||
<f7-accordion-item :opened="collapseStates[transactionType.type].opened"
|
||||
@accordion:open="collapseStates[transactionType.type].opened = true"
|
||||
@accordion:close="collapseStates[transactionType.type].opened = false">
|
||||
<f7-accordion-item :opened="collapseStates[categoryType.type].opened"
|
||||
@accordion:open="collapseStates[categoryType.type].opened = true"
|
||||
@accordion:close="collapseStates[categoryType.type].opened = false">
|
||||
<f7-block-title>
|
||||
<f7-accordion-toggle>
|
||||
<f7-list strong inset dividers media-list
|
||||
class="combination-list-header"
|
||||
:class="collapseStates[transactionType.type].opened ? 'combination-list-opened' : 'combination-list-closed'">
|
||||
:class="collapseStates[categoryType.type].opened ? 'combination-list-opened' : 'combination-list-closed'">
|
||||
<f7-list-item>
|
||||
<template #title>
|
||||
<span>{{ getCategoryTypeName(transactionType.type) }}</span>
|
||||
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[transactionType.type].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
|
||||
<span>{{ getCategoryTypeName(categoryType.type) }}</span>
|
||||
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[categoryType.type].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-accordion-toggle>
|
||||
</f7-block-title>
|
||||
<f7-accordion-content :style="{ height: collapseStates[transactionType.type].opened ? 'auto' : '' }">
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content" v-if="!hasAvailableCategory[transactionType.type]">
|
||||
<f7-list-item :title="$t('No available category')"></f7-list-item>
|
||||
<f7-accordion-content :style="{ height: collapseStates[categoryType.type].opened ? 'auto' : '' }">
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content" v-if="!hasAvailableCategory[categoryType.type]">
|
||||
<f7-list-item :title="tt('No available category')"></f7-list-item>
|
||||
</f7-list>
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content" v-else-if="hasAvailableCategory[transactionType.type]">
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content" v-else-if="hasAvailableCategory[categoryType.type]">
|
||||
<f7-list-item checkbox
|
||||
:class="{ 'has-child-list-item': (showHidden && transactionType.allSubCategories[category.id]) || transactionType.allVisibleSubCategoryCounts[category.id] }"
|
||||
:class="{ 'has-child-list-item': (showHidden && categoryType.allSubCategories[category.id]) || categoryType.allVisibleSubCategoryCounts[category.id] }"
|
||||
:title="category.name"
|
||||
:value="category.id"
|
||||
:checked="isSubCategoriesAllChecked(category, filterCategoryIds)"
|
||||
:indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
|
||||
:key="category.id"
|
||||
v-for="category in transactionType.allCategories"
|
||||
v-for="category in categoryType.allCategories"
|
||||
v-show="showHidden || !category.hidden"
|
||||
@change="selectSubCategories">
|
||||
@change="updateAllSubCategoriesSelected">
|
||||
<template #media>
|
||||
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color">
|
||||
<f7-badge color="gray" class="right-bottom-icon" v-if="category.hidden">
|
||||
@@ -96,15 +96,15 @@
|
||||
|
||||
<template #root>
|
||||
<ul class="padding-left"
|
||||
v-if="(showHidden && transactionType.allSubCategories[category.id]) || transactionType.allVisibleSubCategoryCounts[category.id]">
|
||||
v-if="(showHidden && categoryType.allSubCategories[category.id]) || categoryType.allVisibleSubCategoryCounts[category.id]">
|
||||
<f7-list-item checkbox
|
||||
:title="subCategory.name"
|
||||
:value="subCategory.id"
|
||||
:checked="isCategoryChecked(subCategory, filterCategoryIds)"
|
||||
:key="subCategory.id"
|
||||
v-for="subCategory in transactionType.allSubCategories[category.id]"
|
||||
v-for="subCategory in categoryType.allSubCategories[category.id]"
|
||||
v-show="showHidden || !subCategory.hidden"
|
||||
@change="selectCategory">
|
||||
@change="updateCategorySelected">
|
||||
<template #media>
|
||||
<ItemIcon icon-type="category" :icon-id="subCategory.icon" :color="subCategory.color">
|
||||
<f7-badge color="gray" class="right-bottom-icon" v-if="subCategory.hidden">
|
||||
@@ -123,259 +123,149 @@
|
||||
|
||||
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
|
||||
<f7-actions-group>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectAll">{{ $t('Select All') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectNone">{{ $t('Select None') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectInvert">{{ $t('Invert Selection') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectAllCategories">{{ tt('Select All') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectNoneCategories">{{ tt('Select None') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleCategory }" @click="selectInvertCategories">{{ tt('Invert Selection') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button v-if="!showHidden" @click="showHidden = true">{{ $t('Show Hidden Transaction Categories') }}</f7-actions-button>
|
||||
<f7-actions-button v-if="showHidden" @click="showHidden = false">{{ $t('Hide Hidden Transaction Categories') }}</f7-actions-button>
|
||||
<f7-actions-button v-if="!showHidden" @click="showHidden = true">{{ tt('Show Hidden Transaction Categories') }}</f7-actions-button>
|
||||
<f7-actions-button v-if="showHidden" @click="showHidden = false">{{ tt('Hide Hidden Transaction Categories') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button>
|
||||
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
</f7-actions>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapStores } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/setting.ts';
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Router } from 'framework7/types';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useI18nUIComponents } from '@/lib/ui/mobile.ts';
|
||||
import { useCategoryFilterSettingPageBase } from '@/views/base/settings/CategoryFilterSettingPageBase.ts';
|
||||
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
import { useStatisticsStore } from '@/stores/statistics.ts';
|
||||
|
||||
import { CategoryType } from '@/core/category.ts';
|
||||
import { copyObjectTo, arrayItemToObjectField } from '@/lib/common.ts';
|
||||
|
||||
import {
|
||||
allTransactionCategoriesWithVisibleCount,
|
||||
hasAnyAvailableCategory,
|
||||
hasAvailableCategory,
|
||||
selectSubCategories,
|
||||
selectAllSubCategories,
|
||||
selectAll,
|
||||
selectNone,
|
||||
selectInvert,
|
||||
isCategoryOrSubCategoriesAllChecked,
|
||||
isSubCategoriesAllChecked,
|
||||
isSubCategoriesHasButNotAllChecked
|
||||
} from '@/lib/category.ts';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'f7route',
|
||||
'f7router'
|
||||
],
|
||||
data: function () {
|
||||
return {
|
||||
loading: true,
|
||||
loadingError: null,
|
||||
type: null,
|
||||
allowCategoryTypes: null,
|
||||
filterCategoryIds: {},
|
||||
showHidden: false,
|
||||
collapseStates: {
|
||||
[CategoryType.Income]: {
|
||||
opened: true
|
||||
},
|
||||
[CategoryType.Expense]: {
|
||||
opened: true
|
||||
},
|
||||
[CategoryType.Transfer]: {
|
||||
opened: true
|
||||
}
|
||||
},
|
||||
showMoreActionSheet: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useTransactionCategoriesStore, useTransactionsStore, useStatisticsStore),
|
||||
title() {
|
||||
if (this.type === 'statisticsDefault') {
|
||||
return 'Default Transaction Category Filter';
|
||||
} else {
|
||||
return 'Filter Transaction Categories';
|
||||
}
|
||||
},
|
||||
applyText() {
|
||||
if (this.type === 'statisticsDefault') {
|
||||
return 'Save';
|
||||
} else {
|
||||
return 'Apply';
|
||||
}
|
||||
},
|
||||
allTransactionCategories() {
|
||||
return allTransactionCategoriesWithVisibleCount(this.transactionCategoriesStore.allTransactionCategories, this.allowCategoryTypes);
|
||||
},
|
||||
hasAnyAvailableCategory() {
|
||||
return hasAnyAvailableCategory(this.allTransactionCategories, true);
|
||||
},
|
||||
hasAnyVisibleCategory() {
|
||||
return hasAnyAvailableCategory(this.allTransactionCategories, this.showHidden);
|
||||
},
|
||||
hasAvailableCategory() {
|
||||
return hasAvailableCategory(this.allTransactionCategories, this.showHidden);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
const query = self.f7route.query;
|
||||
|
||||
self.type = query.type;
|
||||
self.allowCategoryTypes = query.allowCategoryTypes ? arrayItemToObjectField(query.allowCategoryTypes.split(','), true) : null;
|
||||
|
||||
self.transactionCategoriesStore.loadAllCategories({
|
||||
force: false
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
|
||||
const allCategoryIds = {};
|
||||
|
||||
for (const categoryId in self.transactionCategoriesStore.allTransactionCategoriesMap) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.transactionCategoriesStore.allTransactionCategoriesMap, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = self.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (self.allowCategoryTypes && !self.allowCategoryTypes[category.type]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self.type === 'transactionListCurrent' && self.transactionsStore.allFilterCategoryIdsCount > 0) {
|
||||
allCategoryIds[category.id] = true;
|
||||
} else {
|
||||
allCategoryIds[category.id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.type === 'statisticsDefault') {
|
||||
self.filterCategoryIds = copyObjectTo(self.settingsStore.appSettings.statistics.defaultTransactionCategoryFilter, allCategoryIds);
|
||||
} else if (self.type === 'statisticsCurrent') {
|
||||
self.filterCategoryIds = copyObjectTo(self.statisticsStore.transactionStatisticsFilter.filterCategoryIds, allCategoryIds);
|
||||
} else if (self.type === 'transactionListCurrent') {
|
||||
for (const categoryId in self.transactionsStore.allFilterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.transactionsStore.allFilterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = self.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (category && (!category.subCategories || !category.subCategories.length)) {
|
||||
allCategoryIds[category.id] = false;
|
||||
} else if (category) {
|
||||
selectSubCategories(allCategoryIds, category, false);
|
||||
}
|
||||
}
|
||||
|
||||
self.filterCategoryIds = allCategoryIds;
|
||||
} else {
|
||||
self.$toast('Parameter Invalid');
|
||||
self.loadingError = 'Parameter Invalid';
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error.processed) {
|
||||
self.loading = false;
|
||||
} else {
|
||||
self.loadingError = error;
|
||||
self.$toast(error.message || error);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn() {
|
||||
this.$routeBackOnError(this.f7router, 'loadingError');
|
||||
},
|
||||
save() {
|
||||
const self = this;
|
||||
const router = self.f7router;
|
||||
|
||||
const filteredCategoryIds = {};
|
||||
let isAllSelected = true;
|
||||
let finalCategoryIds = '';
|
||||
|
||||
for (const categoryId in self.filterCategoryIds) {
|
||||
if (!Object.prototype.hasOwnProperty.call(self.filterCategoryIds, categoryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = self.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (!isCategoryOrSubCategoriesAllChecked(category, self.filterCategoryIds)) {
|
||||
filteredCategoryIds[categoryId] = true;
|
||||
isAllSelected = false;
|
||||
} else {
|
||||
if (finalCategoryIds.length > 0) {
|
||||
finalCategoryIds += ',';
|
||||
}
|
||||
|
||||
finalCategoryIds += categoryId;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type === 'statisticsDefault') {
|
||||
self.settingsStore.setStatisticsDefaultTransactionCategoryFilter(filteredCategoryIds);
|
||||
} else if (this.type === 'statisticsCurrent') {
|
||||
self.statisticsStore.updateTransactionStatisticsFilter({
|
||||
filterCategoryIds: filteredCategoryIds
|
||||
});
|
||||
} else if (this.type === 'transactionListCurrent') {
|
||||
const changed = self.transactionsStore.updateTransactionListFilter({
|
||||
categoryIds: isAllSelected ? '' : finalCategoryIds
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
self.transactionsStore.updateTransactionListInvalidState(true);
|
||||
}
|
||||
}
|
||||
|
||||
router.back();
|
||||
},
|
||||
selectCategory(e) {
|
||||
const categoryId = e.target.value;
|
||||
const category = this.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterCategoryIds[category.id] = !e.target.checked;
|
||||
},
|
||||
selectSubCategories(e) {
|
||||
const categoryId = e.target.value;
|
||||
const category = this.transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
selectSubCategories(this.filterCategoryIds, category, !e.target.checked);
|
||||
},
|
||||
selectAll() {
|
||||
selectAll(this.filterCategoryIds, this.transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
},
|
||||
selectNone() {
|
||||
selectNone(this.filterCategoryIds, this.transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
},
|
||||
selectInvert() {
|
||||
selectInvert(this.filterCategoryIds, this.transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
},
|
||||
getCategoryTypeName(categoryType) {
|
||||
switch (categoryType) {
|
||||
case CategoryType.Income:
|
||||
return this.$t('Income Categories');
|
||||
case CategoryType.Expense:
|
||||
return this.$t('Expense Categories');
|
||||
case CategoryType.Transfer:
|
||||
return this.$t('Transfer Categories');
|
||||
default:
|
||||
return this.$t('Transaction Categories');
|
||||
}
|
||||
},
|
||||
isCategoryChecked(category, filterCategoryIds) {
|
||||
return !filterCategoryIds[category.id];
|
||||
},
|
||||
isSubCategoriesAllChecked(category, filterCategoryIds) {
|
||||
return isSubCategoriesAllChecked(category, filterCategoryIds);
|
||||
},
|
||||
isSubCategoriesHasButNotAllChecked(category, filterCategoryIds) {
|
||||
return isSubCategoriesHasButNotAllChecked(category, filterCategoryIds);
|
||||
}
|
||||
}
|
||||
interface CollapseState {
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
f7route: Router.Route;
|
||||
f7router: Router.Router;
|
||||
}>();
|
||||
|
||||
const query = props.f7route.query;
|
||||
|
||||
const { tt } = useI18n();
|
||||
const { showToast, routeBackOnError } = useI18nUIComponents();
|
||||
|
||||
const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterCategoryIds,
|
||||
title,
|
||||
applyText,
|
||||
allTransactionCategories,
|
||||
hasAnyAvailableCategory,
|
||||
hasAnyVisibleCategory,
|
||||
hasAvailableCategory,
|
||||
isCategoryChecked,
|
||||
getCategoryTypeName,
|
||||
loadFilterCategoryIds,
|
||||
saveFilterCategoryIds
|
||||
} = useCategoryFilterSettingPageBase(query['type'], query['allowCategoryTypes']);
|
||||
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const showMoreActionSheet = ref<boolean>(false);
|
||||
|
||||
const collapseStates = ref<Record<number, CollapseState>>({
|
||||
[CategoryType.Income]: {
|
||||
opened: true
|
||||
},
|
||||
[CategoryType.Expense]: {
|
||||
opened: true
|
||||
},
|
||||
[CategoryType.Transfer]: {
|
||||
opened: true
|
||||
}
|
||||
});
|
||||
|
||||
function init(): void {
|
||||
transactionCategoriesStore.loadAllCategories({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
|
||||
if (!loadFilterCategoryIds()) {
|
||||
showToast('Parameter Invalid');
|
||||
loadingError.value = 'Parameter Invalid';
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error.processed) {
|
||||
loading.value = false;
|
||||
} else {
|
||||
loadingError.value = error;
|
||||
showToast(error.message || error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateCategorySelected(e: Event): void {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const categoryId = target.value;
|
||||
const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
filterCategoryIds.value[category.id] = !target.checked;
|
||||
}
|
||||
|
||||
function updateAllSubCategoriesSelected(e: Event): void {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const categoryId = target.value;
|
||||
const category = transactionCategoriesStore.allTransactionCategoriesMap[categoryId];
|
||||
|
||||
selectAllSubCategories(filterCategoryIds.value, category, !target.checked);
|
||||
}
|
||||
|
||||
function selectAllCategories(): void {
|
||||
selectAll(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
}
|
||||
|
||||
function selectNoneCategories(): void {
|
||||
selectNone(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
}
|
||||
|
||||
function selectInvertCategories(): void {
|
||||
selectInvert(filterCategoryIds.value, transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
saveFilterCategoryIds();
|
||||
props.f7router.back();
|
||||
}
|
||||
|
||||
function onPageAfterIn(): void {
|
||||
routeBackOnError(props.f7router, loadingError);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user