tag filter supports selecting both included and excluded tags simultaneously

This commit is contained in:
MaysWind
2025-11-24 02:12:44 +08:00
parent 45be96cf68
commit 6430a52027
45 changed files with 1151 additions and 706 deletions
@@ -1,26 +1,35 @@
import { ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { useStatisticsStore } from '@/stores/statistics.ts';
import { type TypeAndDisplayName, keys, keysIfValueEquals, values } from '@/core/base.ts';
import { entries, values } from '@/core/base.ts';
import { TransactionTagFilterType } from '@/core/transaction.ts';
import type { TransactionTag } from '@/models/transaction_tag.ts';
import { TransactionTagFilter } from '@/models/transaction.ts';
import { objectFieldWithValueToArrayItem } from '@/lib/common.ts';
export enum TransactionTagFilterState {
Default = 0,
Include = 1,
Exclude = 2
}
export function useTransactionTagFilterSettingPageBase(type?: string) {
const { getAllTransactionTagFilterTypes } = useI18n();
const transactionTagsStore = useTransactionTagsStore();
const transactionsStore = useTransactionsStore();
const statisticsStore = useStatisticsStore();
const loading = ref<boolean>(true);
const showHidden = ref<boolean>(false);
const filterTagIds = ref<Record<string, boolean>>({});
const tagFilterType = ref<number>(TransactionTagFilterType.Default.type);
const filterTagIds = ref<Record<string, TransactionTagFilterState>>({});
const includeTagFilterType = ref<number>(TransactionTagFilterType.HasAny.type);
const excludeTagFilterType = ref<number>(TransactionTagFilterType.NotHasAny.type);
const includeTagsCount = computed<number>(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Include).length);
const excludeTagsCount = computed<number>(() => objectFieldWithValueToArrayItem(filterTagIds.value, TransactionTagFilterState.Exclude).length);
const title = computed<string>(() => {
return 'Filter Transaction Tags';
@@ -31,7 +40,6 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
});
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const allTagFilterTypes = computed<TypeAndDisplayName[]>(() => getAllTransactionTagFilterTypes());
const hasAnyAvailableTag = computed<boolean>(() => transactionTagsStore.allAvailableTagsCount > 0);
const hasAnyVisibleTag = computed<boolean>(() => {
if (showHidden.value) {
@@ -42,67 +50,76 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
});
function loadFilterTagIds(): boolean {
const allTransactionTagIds: Record<string, boolean> = {};
for (const transactionTag of values(transactionTagsStore.allTransactionTagsMap)) {
allTransactionTagIds[transactionTag.id] = true;
}
let tagFilters: TransactionTagFilter[] = [];
if (type === 'statisticsCurrent') {
const transactionTagIds = statisticsStore.transactionStatisticsFilter.tagIds ? statisticsStore.transactionStatisticsFilter.tagIds.split(',') : [];
for (const transactionTagId of transactionTagIds) {
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (transactionTag) {
allTransactionTagIds[transactionTag.id] = false;
}
}
filterTagIds.value = allTransactionTagIds;
tagFilterType.value = statisticsStore.transactionStatisticsFilter.tagFilterType;
return true;
tagFilters = TransactionTagFilter.parse(statisticsStore.transactionStatisticsFilter.tagFilter);
} else if (type === 'transactionListCurrent') {
for (const transactionTagId of keysIfValueEquals(transactionsStore.allFilterTagIds, true)) {
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (transactionTag) {
allTransactionTagIds[transactionTag.id] = false;
}
}
filterTagIds.value = allTransactionTagIds;
return true;
tagFilters = TransactionTagFilter.parse(transactionsStore.transactionsFilter.tagFilter);
} else {
return false;
}
const allTagIdsMap: Record<string, TransactionTagFilterState> = {};
for (const transactionTag of values(transactionTagsStore.allTransactionTagsMap)) {
allTagIdsMap[transactionTag.id] = TransactionTagFilterState.Default;
}
for (const tagFilter of tagFilters) {
let state: TransactionTagFilterState = TransactionTagFilterState.Default;
if (tagFilter.type === TransactionTagFilterType.HasAny || tagFilter.type === TransactionTagFilterType.HasAll) {
state = TransactionTagFilterState.Include;
includeTagFilterType.value = tagFilter.type.type;
} else if (tagFilter.type === TransactionTagFilterType.NotHasAny || tagFilter.type === TransactionTagFilterType.NotHasAll) {
state = TransactionTagFilterState.Exclude;
excludeTagFilterType.value = tagFilter.type.type;
} else {
continue;
}
for (const tagId of tagFilter.tagIds) {
allTagIdsMap[tagId] = state;
}
}
filterTagIds.value = allTagIdsMap;
return true;
}
function saveFilterTagIds(): boolean {
const filteredTagIds: Record<string, boolean> = {};
let finalTagIds = '';
const includeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(includeTagFilterType.value) ?? TransactionTagFilterType.HasAny);
const excludeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(excludeTagFilterType.value) ?? TransactionTagFilterType.NotHasAny);
let changed = true;
for (const transactionTagId of keys(filterTagIds.value)) {
for (const [transactionTagId, state] of entries(filterTagIds.value)) {
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (!transactionTag) {
continue;
}
if (filterTagIds.value[transactionTag.id]) {
filteredTagIds[transactionTag.id] = true;
} else {
if (finalTagIds.length > 0) {
finalTagIds += ',';
}
finalTagIds += transactionTag.id;
if (state === TransactionTagFilterState.Include) {
includeTagFilter.tagIds.push(transactionTag.id);
} else if (state === TransactionTagFilterState.Exclude) {
excludeTagFilter.tagIds.push(transactionTag.id);
}
}
const tagFilters: TransactionTagFilter[] = [];
if (includeTagFilter.tagIds.length > 0) {
tagFilters.push(includeTagFilter);
}
if (excludeTagFilter.tagIds.length > 0) {
tagFilters.push(excludeTagFilter);
}
if (type === 'statisticsCurrent') {
changed = statisticsStore.updateTransactionStatisticsFilter({
tagIds: finalTagIds,
tagFilterType: tagFilterType.value
tagFilter: TransactionTagFilter.toTextualTagFilters(tagFilters)
});
if (changed) {
@@ -110,7 +127,7 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
}
} else if (type === 'transactionListCurrent') {
changed = transactionsStore.updateTransactionListFilter({
tagIds: finalTagIds
tagFilter: TransactionTagFilter.toTextualTagFilters(tagFilters)
});
if (changed) {
@@ -126,12 +143,14 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
loading,
showHidden,
filterTagIds,
tagFilterType,
includeTagFilterType,
excludeTagFilterType,
// computed states
includeTagsCount,
excludeTagsCount,
title,
applyText,
allTags,
allTagFilterTypes,
hasAnyAvailableTag,
hasAnyVisibleTag,
// functions
@@ -9,7 +9,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { type TransactionListFilter, type TransactionMonthList, useTransactionsStore } from '@/stores/transaction.ts';
import { type TypeAndName, entries } from '@/core/base.ts';
import { type TypeAndName, keys, entries } from '@/core/base.ts';
import type { NumeralSystem } from '@/core/numeral.ts';
import { type TextualYearMonthDay, type Year0BasedMonth, type LocalizedDateRange, type WeekDayValue, DateRange, DateRangeScene } from '@/core/datetime.ts';
import { AccountType } from '@/core/account.ts';
@@ -19,7 +19,7 @@ import { DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numera
import type { Account } from '@/models/account.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import type { TransactionTag } from '@/models/transaction_tag.ts';
import type { Transaction } from '@/models/transaction.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import {
getUtcOffsetByUtcOffsetMinutes,
@@ -196,7 +196,7 @@ export function useTransactionListPageBase() {
});
const queryTagName = computed<string>(() => {
if (query.value.tagIds === 'none') {
if (query.value.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue) {
return tt('Without Tags');
}
@@ -204,7 +204,15 @@ export function useTransactionListPageBase() {
return tt('Multiple Tags');
}
return allTransactionTags.value[query.value.tagIds]?.name || tt('Tags');
for (const tagId of keys(queryAllFilterTagIds.value)) {
const tagName = allTransactionTags.value[tagId]?.name;
if (tagName) {
return tagName;
}
}
return tt('Tags');
});
const queryAmount = computed<string>(() => {