mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 16:07:33 +08:00
tag filter supports selecting both included and excluded tags simultaneously
This commit is contained in:
@@ -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>(() => {
|
||||
|
||||
@@ -11,22 +11,30 @@
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Select All')"
|
||||
:title="tt('Set All to Included')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectAllTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Include)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelect"
|
||||
:title="tt('Select None')"
|
||||
:title="tt('Set All to Default')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectNoneTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Default)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectInverse"
|
||||
:title="tt('Invert Selection')"
|
||||
:title="tt('Set All to Excluded')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectInvertTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Exclude)"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Select All Visible')"
|
||||
:title="tt('Set All Visible Items to Included')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectAllVisibleTransactionTags"></v-list-item>
|
||||
@click="setAllToState(true, TransactionTagFilterState.Include)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Set All Visible Items to Default')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="setAllToState(true, TransactionTagFilterState.Default)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Set All Visible Items to Excluded')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="setAllToState(true, TransactionTagFilterState.Exclude)"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
@@ -47,22 +55,30 @@
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Select All')"
|
||||
:title="tt('Set All to Included')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectAllTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Include)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelect"
|
||||
:title="tt('Select None')"
|
||||
:title="tt('Set All to Default')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectNoneTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Default)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectInverse"
|
||||
:title="tt('Invert Selection')"
|
||||
:title="tt('Set All to Excluded')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectInvertTransactionTags"></v-list-item>
|
||||
@click="setAllToState(false, TransactionTagFilterState.Exclude)"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Select All Visible')"
|
||||
:title="tt('Set All Visible Items to Included')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="selectAllVisibleTransactionTags"></v-list-item>
|
||||
@click="setAllToState(true, TransactionTagFilterState.Include)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Set All Visible Items to Default')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="setAllToState(true, TransactionTagFilterState.Default)"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiSelectAll"
|
||||
:title="tt('Set All Visible Items to Excluded')"
|
||||
:disabled="!hasAnyVisibleTag"
|
||||
@click="setAllToState(true, TransactionTagFilterState.Exclude)"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
@@ -86,15 +102,24 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text :class="{ 'mt-0 mt-sm-2 mt-md-4': dialogMode }" v-else-if="!loading && hasAnyVisibleTag">
|
||||
<div class="tag-filter-types d-flex flex-column mb-4" v-if="type === 'statisticsCurrent'">
|
||||
<v-btn border class="justify-start" :key="filterType.type"
|
||||
:color="tagFilterType === filterType.type ? 'primary' : 'default'"
|
||||
:variant="tagFilterType === filterType.type ? 'tonal' : 'outlined'"
|
||||
:append-icon="(tagFilterType === filterType.type ? mdiCheck : undefined)"
|
||||
v-for="filterType in allTagFilterTypes"
|
||||
@click="tagFilterType = filterType.type">
|
||||
{{ filterType.displayName }}
|
||||
</v-btn>
|
||||
<div class="mb-4" v-if="includeTagsCount > 1">
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="includeTagFilterType"
|
||||
@update:model-value="updateTransactionTagIncludeType($event)">
|
||||
<v-btn :value="TransactionTagFilterType.HasAny.type">{{ tt(TransactionTagFilterType.HasAny.name) }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterType.HasAll.type">{{ tt(TransactionTagFilterType.HasAll.name) }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<div class="mb-4" v-if="excludeTagsCount > 1">
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="excludeTagFilterType"
|
||||
@update:model-value="updateTransactionTagExcludeType($event)">
|
||||
<v-btn :value="TransactionTagFilterType.NotHasAny.type">{{ tt(TransactionTagFilterType.NotHasAny.name) }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterType.NotHasAll.type">{{ tt(TransactionTagFilterType.NotHasAll.name) }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<v-expansion-panels class="tag-categories" multiple v-model="expandTagCategories">
|
||||
@@ -108,18 +133,23 @@
|
||||
v-for="transactionTag in allTags">
|
||||
<v-list-item v-if="showHidden || !transactionTag.hidden">
|
||||
<template #prepend>
|
||||
<v-checkbox :model-value="!filterTagIds[transactionTag.id]"
|
||||
@update:model-value="updateTransactionTagSelected(transactionTag, $event)">
|
||||
<template #label>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="2" offset-y="2" :icon="mdiEyeOffOutline"
|
||||
v-if="transactionTag.hidden">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="24" :icon="mdiPound" v-else-if="!transactionTag.hidden"/>
|
||||
<span class="ms-3">{{ transactionTag.name }}</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="2" offset-y="2" :icon="mdiEyeOffOutline"
|
||||
v-if="transactionTag.hidden">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="24" :icon="mdiPound" v-else-if="!transactionTag.hidden"/>
|
||||
<span class="ms-3">{{ transactionTag.name }}</span>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="filterTagIds[transactionTag.id]"
|
||||
@update:model-value="updateTransactionTagState(transactionTag, $event)">
|
||||
<v-btn :value="TransactionTagFilterState.Include">{{ tt('Included') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Default">{{ tt('Default') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Exclude">{{ tt('Excluded') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -146,19 +176,16 @@ import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useTransactionTagFilterSettingPageBase } from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts';
|
||||
import {
|
||||
useTransactionTagFilterSettingPageBase,
|
||||
TransactionTagFilterState
|
||||
} from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
selectAllVisible,
|
||||
selectAll,
|
||||
selectNone,
|
||||
selectInvert
|
||||
} from '@/lib/common.ts';
|
||||
|
||||
import {
|
||||
mdiSelectAll,
|
||||
mdiSelect,
|
||||
@@ -166,7 +193,6 @@ import {
|
||||
mdiEyeOutline,
|
||||
mdiEyeOffOutline,
|
||||
mdiDotsVertical,
|
||||
mdiCheck,
|
||||
mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
@@ -188,11 +214,13 @@ const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterTagIds,
|
||||
tagFilterType,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
title,
|
||||
applyText,
|
||||
allTags,
|
||||
allTagFilterTypes,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
loadFilterTagIds,
|
||||
@@ -223,40 +251,38 @@ function init(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function updateTransactionTagSelected(transactionTag: TransactionTag, value: boolean | null): void {
|
||||
filterTagIds.value[transactionTag.id] = !value;
|
||||
function updateTransactionTagState(transactionTag: TransactionTag, value: TransactionTagFilterState): void {
|
||||
filterTagIds.value[transactionTag.id] = value;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllTransactionTags(): void {
|
||||
selectAll(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
function updateTransactionTagIncludeType(value: number): void {
|
||||
includeTagFilterType.value = value;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function selectNoneTransactionTags(): void {
|
||||
selectNone(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
function updateTransactionTagExcludeType(value: number): void {
|
||||
excludeTagFilterType.value = value;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function selectInvertTransactionTags(): void {
|
||||
selectInvert(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
function setAllToState(onlyVisible: boolean, value: TransactionTagFilterState): void {
|
||||
for (const tag of allTags.value) {
|
||||
if (onlyVisible && !showHidden.value && tag.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
filterTagIds.value[tag.id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllVisibleTransactionTags(): void {
|
||||
selectAllVisible(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
@@ -276,15 +302,9 @@ init();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tag-filter-types .v-btn:not(:first-child) {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
|
||||
.tag-filter-types .v-btn:not(:last-child) {
|
||||
border-bottom: 0;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
.tag-categories .tag-filter-state-toggle {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-categories .v-expansion-panel-text__wrapper {
|
||||
|
||||
@@ -576,8 +576,7 @@ interface TransactionStatisticsProps {
|
||||
initEndTime?: TextualYearMonth | '',
|
||||
initFilterAccountIds?: string,
|
||||
initFilterCategoryIds?: string,
|
||||
initTagIds?: string,
|
||||
initTagFilterType?: string,
|
||||
initTagFilter?: string,
|
||||
initKeyword?: string;
|
||||
initSortingType?: string,
|
||||
initTrendDateAggregationType?: string
|
||||
@@ -757,8 +756,7 @@ function init(initProps: TransactionStatisticsProps): void {
|
||||
chartDataType: initProps.initChartDataType ? parseInt(initProps.initChartDataType) : undefined,
|
||||
filterAccountIds: initProps.initFilterAccountIds ? arrayItemToObjectField(initProps.initFilterAccountIds.split(','), true) : {},
|
||||
filterCategoryIds: initProps.initFilterCategoryIds ? arrayItemToObjectField(initProps.initFilterCategoryIds.split(','), true) : {},
|
||||
tagIds: initProps.initTagIds,
|
||||
tagFilterType: initProps.initTagFilterType && parseInt(initProps.initTagFilterType) >= 0 ? parseInt(initProps.initTagFilterType) : undefined,
|
||||
tagFilter: initProps.initTagFilter,
|
||||
keyword: initProps.initKeyword,
|
||||
sortingType: initProps.initSortingType ? parseInt(initProps.initSortingType) : undefined
|
||||
};
|
||||
@@ -1314,8 +1312,7 @@ onBeforeRouteUpdate((to) => {
|
||||
initEndTime: (to.query['endTime'] as TextualYearMonth | null) || undefined,
|
||||
initFilterAccountIds: (to.query['filterAccountIds'] as string | null) || undefined,
|
||||
initFilterCategoryIds: (to.query['filterCategoryIds'] as string | null) || undefined,
|
||||
initTagIds: (to.query['tagIds'] as string | null) || undefined,
|
||||
initTagFilterType: (to.query['tagFilterType'] as string | null) || undefined,
|
||||
initTagFilter: (to.query['tagFilter'] as string | null) || undefined,
|
||||
initKeyword: (to.query['keyword'] as string | null) || undefined,
|
||||
initSortingType: (to.query['sortingType'] as string | null) || undefined,
|
||||
initTrendDateAggregationType: (to.query['trendDateAggregationType'] as string | null) || undefined,
|
||||
|
||||
@@ -431,15 +431,15 @@
|
||||
@update:model-value="scrollTagMenuToSelectedItem">
|
||||
<template #activator="{ props }">
|
||||
<div class="d-flex align-center cursor-pointer"
|
||||
:class="{ 'readonly': loading, 'text-primary': query.tagIds }" v-bind="props">
|
||||
:class="{ 'readonly': loading, 'text-primary': query.tagFilter }" v-bind="props">
|
||||
<span>{{ queryTagName }}</span>
|
||||
<v-icon :icon="mdiMenuDown" />
|
||||
</div>
|
||||
</template>
|
||||
<v-list :selected="[queryAllSelectedFilterTagIds]">
|
||||
<v-list-item key="" value="" class="text-sm" density="compact"
|
||||
:class="{ 'list-item-selected': !query.tagIds }"
|
||||
:append-icon="(!query.tagIds ? mdiCheck : undefined)">
|
||||
:class="{ 'list-item-selected': !query.tagFilter }"
|
||||
:append-icon="(!query.tagFilter ? mdiCheck : undefined)">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilter('')">
|
||||
<div class="d-flex align-center">
|
||||
@@ -448,11 +448,13 @@
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item key="none" value="none" class="text-sm" density="compact"
|
||||
:class="{ 'list-item-selected': query.tagIds === 'none' }"
|
||||
:append-icon="(query.tagIds === 'none' ? mdiCheck : undefined)">
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:key="TransactionTagFilter.TransactionNoTagFilterValue"
|
||||
:value="TransactionTagFilter.TransactionNoTagFilterValue"
|
||||
:class="{ 'list-item-selected': query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue }"
|
||||
:append-icon="(query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue ? mdiCheck : undefined)">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilter('none')">
|
||||
@click="changeTagFilter(TransactionTagFilter.TransactionNoTagFilterValue)">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :icon="mdiBorderNoneVariant" />
|
||||
<span class="text-sm ms-3">{{ tt('Without Tags') }}</span>
|
||||
@@ -460,8 +462,8 @@
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item key="multiple" value="multiple" class="text-sm" density="compact"
|
||||
:class="{ 'list-item-selected': query.tagIds && queryAllFilterTagIdsCount > 1 }"
|
||||
:append-icon="(query.tagIds && queryAllFilterTagIdsCount > 1 ? mdiCheck : undefined)"
|
||||
:class="{ 'list-item-selected': query.tagFilter && queryAllFilterTagIdsCount > 1 }"
|
||||
:append-icon="(query.tagFilter && queryAllFilterTagIdsCount > 1 ? mdiCheck : undefined)"
|
||||
v-if="allAvailableTagsCount > 0">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="showFilterTagDialog = true">
|
||||
@@ -472,34 +474,18 @@
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="query.tagIds && query.tagIds !== 'none'" />
|
||||
|
||||
<template v-if="query.tagIds && query.tagIds !== 'none'">
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:key="filterType.type"
|
||||
:value="filterType.type"
|
||||
:append-icon="(query.tagFilterType === filterType.type ? mdiCheck : undefined)"
|
||||
v-for="filterType in allTransactionTagFilterTypes">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilterType(filterType.type)">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" :icon="filterType.icon"/>
|
||||
<span class="text-sm ms-3">{{ filterType.displayName }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-divider v-if="query.tagFilter && query.tagFilter !== TransactionTagFilter.TransactionNoTagFilterValue" />
|
||||
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allTransactionTags">
|
||||
<v-divider v-if="!transactionTag.hidden || query.tagIds === transactionTag.id" />
|
||||
<v-divider v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="transactionTag.id"
|
||||
:class="{ 'list-item-selected': query.tagIds === transactionTag.id, 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && queryAllFilterTagIds[transactionTag.id] }"
|
||||
:append-icon="(query.tagIds === transactionTag.id ? mdiCheck : undefined)"
|
||||
v-if="!transactionTag.hidden || query.tagIds === transactionTag.id">
|
||||
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
|
||||
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
|
||||
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilter(transactionTag.id)">
|
||||
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
|
||||
@@ -678,7 +664,6 @@ import { useDesktopPageStore } from '@/stores/desktopPage.ts';
|
||||
|
||||
import {
|
||||
type NameNumeralValue,
|
||||
type TypeAndDisplayName,
|
||||
keys
|
||||
} from '@/core/base.ts';
|
||||
import {
|
||||
@@ -690,16 +675,18 @@ import {
|
||||
} from '@/core/datetime.ts';
|
||||
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
|
||||
import { ThemeType } from '@/core/theme.ts';
|
||||
import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import { TemplateType } from '@/core/template.ts';
|
||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import type { Transaction } from '@/models/transaction.ts';
|
||||
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
||||
import type { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||
|
||||
import {
|
||||
isDefined,
|
||||
isObject,
|
||||
isString,
|
||||
isNumber
|
||||
isNumber,
|
||||
objectFieldWithValueToArrayItem
|
||||
} from '@/lib/common.ts';
|
||||
import {
|
||||
getCurrentUnixTime,
|
||||
@@ -731,6 +718,7 @@ import logger from '@/lib/logger.ts';
|
||||
import {
|
||||
mdiMagnify,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiViewGridOutline,
|
||||
mdiBorderNoneVariant,
|
||||
mdiVectorArrangeBelow,
|
||||
@@ -740,10 +728,6 @@ import {
|
||||
mdiPencilBoxOutline,
|
||||
mdiArrowLeft,
|
||||
mdiArrowRight,
|
||||
mdiPlusBoxMultipleOutline,
|
||||
mdiCheckboxMultipleOutline,
|
||||
mdiMinusBoxMultipleOutline,
|
||||
mdiCloseBoxMultipleOutline,
|
||||
mdiPound,
|
||||
mdiMagicStaff,
|
||||
mdiTextBoxOutline
|
||||
@@ -757,8 +741,7 @@ interface TransactionListProps {
|
||||
initType?: string,
|
||||
initCategoryIds?: string,
|
||||
initAccountIds?: string,
|
||||
initTagIds?: string,
|
||||
initTagFilterType?: string,
|
||||
initTagFilter?: string,
|
||||
initAmountFilter?: string,
|
||||
initKeyword?: string
|
||||
}
|
||||
@@ -771,12 +754,6 @@ type EditDialogType = InstanceType<typeof EditDialog>;
|
||||
type AIImageRecognitionDialogType = InstanceType<typeof AIImageRecognitionDialog>;
|
||||
type ImportDialogType = InstanceType<typeof ImportDialog>;
|
||||
|
||||
interface TransactionTemplateWithIcon {
|
||||
type: number;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface TransactionListDisplayTotalAmount {
|
||||
income: string;
|
||||
expense: string;
|
||||
@@ -789,7 +766,6 @@ const theme = useTheme();
|
||||
const {
|
||||
tt,
|
||||
getAllRecentMonthDateRanges,
|
||||
getAllTransactionTagFilterTypes,
|
||||
getWeekdayLongName,
|
||||
getCurrentNumeralSystemType
|
||||
} = useI18n();
|
||||
@@ -852,13 +828,6 @@ const transactionsStore = useTransactionsStore();
|
||||
const transactionTemplatesStore = useTransactionTemplatesStore();
|
||||
const desktopPageStore = useDesktopPageStore();
|
||||
|
||||
const tagFilterIconMap: Record<number, string> = {
|
||||
[TransactionTagFilterType.HasAny.type]: mdiPlusBoxMultipleOutline,
|
||||
[TransactionTagFilterType.HasAll.type]: mdiCheckboxMultipleOutline,
|
||||
[TransactionTagFilterType.NotHasAny.type]: mdiMinusBoxMultipleOutline,
|
||||
[TransactionTagFilterType.NotHasAll.type]: mdiCloseBoxMultipleOutline
|
||||
};
|
||||
|
||||
const timeFilterMenu = useTemplateRef<VMenu>('timeFilterMenu');
|
||||
const categoryFilterMenu = useTemplateRef<VMenu>('categoryFilterMenu');
|
||||
const amountFilterMenu = useTemplateRef<VMenu>('amountFilterMenu');
|
||||
@@ -912,21 +881,6 @@ const allTransactionTemplates = computed<TransactionTemplate[]>(() => {
|
||||
return allTemplates[TemplateType.Normal.type] || [];
|
||||
});
|
||||
|
||||
const allTransactionTagFilterTypes = computed<TransactionTemplateWithIcon[]>(() => {
|
||||
const allTagFilterTypes: TypeAndDisplayName[] = getAllTransactionTagFilterTypes();
|
||||
const allTagFilterTypesWithIcon: TransactionTemplateWithIcon[] = [];
|
||||
|
||||
for (const tagFilterType of allTagFilterTypes) {
|
||||
allTagFilterTypesWithIcon.push({
|
||||
type: tagFilterType.type,
|
||||
displayName: tagFilterType.displayName,
|
||||
icon: tagFilterIconMap[tagFilterType.type] ?? ''
|
||||
});
|
||||
}
|
||||
|
||||
return allTagFilterTypesWithIcon;
|
||||
});
|
||||
|
||||
const allowCategoryTypes = computed<string>(() => {
|
||||
if (TransactionType.Income <= query.value.type && query.value.type <= TransactionType.Transfer) {
|
||||
return transactionTypeToCategoryType(query.value.type)?.toString() ?? '';
|
||||
@@ -1018,10 +972,16 @@ const queryAllSelectedFilterAccountIds = computed<string>(() => {
|
||||
});
|
||||
|
||||
const queryAllSelectedFilterTagIds = computed<string>(() => {
|
||||
if (queryAllFilterTagIdsCount.value === 0) {
|
||||
if (query.value.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue) {
|
||||
return TransactionTagFilter.TransactionNoTagFilterValue;
|
||||
} else if (queryAllFilterTagIdsCount.value === 0) {
|
||||
return '';
|
||||
} else if (queryAllFilterTagIdsCount.value === 1) {
|
||||
return query.value.tagIds;
|
||||
for (const tagId of keys(queryAllFilterTagIds.value)) {
|
||||
return tagId;
|
||||
}
|
||||
|
||||
return '';
|
||||
} else { // queryAllFilterTagIdsCount.value > 1
|
||||
return 'multiple';
|
||||
}
|
||||
@@ -1147,8 +1107,7 @@ function init(initProps: TransactionListProps): void {
|
||||
type: initProps.initType && parseInt(initProps.initType) > 0 ? parseInt(initProps.initType) : undefined,
|
||||
categoryIds: initProps.initCategoryIds,
|
||||
accountIds: initProps.initAccountIds,
|
||||
tagIds: initProps.initTagIds,
|
||||
tagFilterType: initProps.initTagFilterType && parseInt(initProps.initTagFilterType) >= 0 ? parseInt(initProps.initTagFilterType) : undefined,
|
||||
tagFilter: initProps.initTagFilter,
|
||||
amountFilter: initProps.initAmountFilter || '',
|
||||
keyword: initProps.initKeyword || ''
|
||||
});
|
||||
@@ -1490,13 +1449,13 @@ function changeMultipleAccountsFilter(changed: boolean): void {
|
||||
updateUrlWhenChanged(changed);
|
||||
}
|
||||
|
||||
function changeTagFilter(tagIds: string): void {
|
||||
if (query.value.tagIds === tagIds) {
|
||||
function changeTagFilter(tagFilter: string): void {
|
||||
if (query.value.tagFilter === tagFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changed = transactionsStore.updateTransactionListFilter({
|
||||
tagIds: tagIds
|
||||
tagFilter: tagFilter
|
||||
});
|
||||
|
||||
updateUrlWhenChanged(changed);
|
||||
@@ -1508,18 +1467,6 @@ function changeMultipleTagsFilter(changed: boolean): void {
|
||||
updateUrlWhenChanged(changed);
|
||||
}
|
||||
|
||||
function changeTagFilterType(filterType: number): void {
|
||||
if (query.value.tagFilterType === filterType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changed = transactionsStore.updateTransactionListFilter({
|
||||
tagFilterType: filterType
|
||||
});
|
||||
|
||||
updateUrlWhenChanged(changed);
|
||||
}
|
||||
|
||||
function changeKeywordFilter(keyword: string): void {
|
||||
if (query.value.keyword === keyword) {
|
||||
return;
|
||||
@@ -1592,7 +1539,7 @@ function add(template?: TransactionTemplate): void {
|
||||
type: query.value.type,
|
||||
categoryId: queryAllFilterCategoryIdsCount.value === 1 ? query.value.categoryIds : '',
|
||||
accountId: queryAllFilterAccountIdsCount.value === 1 ? query.value.accountIds : '',
|
||||
tagIds: query.value.tagIds || '',
|
||||
tagIds: objectFieldWithValueToArrayItem(queryAllFilterTagIds.value, true).join(',') || '',
|
||||
template: template
|
||||
}).then(result => {
|
||||
if (result && result.message) {
|
||||
@@ -1765,8 +1712,7 @@ onBeforeRouteUpdate((to) => {
|
||||
initType: (to.query['type'] as string | null) || undefined,
|
||||
initCategoryIds: (to.query['categoryIds'] as string | null) || undefined,
|
||||
initAccountIds: (to.query['accountIds'] as string | null) || undefined,
|
||||
initTagIds: (to.query['tagIds'] as string | null) || undefined,
|
||||
initTagFilterType: (to.query['tagFilterType'] as string | null) || undefined,
|
||||
initTagFilter: (to.query['tagFilter'] as string | null) || undefined,
|
||||
initAmountFilter: (to.query['amountFilter'] as string | null) || undefined,
|
||||
initKeyword: (to.query['keyword'] as string | null) || undefined
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
v-for="typeName in parsedFileAllTransactionTypes">
|
||||
<td>{{ typeName }}</td>
|
||||
<td>
|
||||
<v-btn-toggle class="transaction-types-toggle" density="compact" variant="outlined"
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
v-model="parsedFileDataColumnMapping.transactionTypeMapping[typeName]">
|
||||
<v-btn :value="undefined">{{ tt('None') }}</v-btn>
|
||||
@@ -166,14 +166,14 @@
|
||||
v-for="separator in allSeparators">
|
||||
<td>{{ separator.name }} ({{separator.value}})</td>
|
||||
<td>
|
||||
<v-btn-toggle class="transaction-types-toggle" density="compact" variant="outlined"
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
v-model="parsedFileDataColumnMapping.geoLocationOrder"
|
||||
v-if="parsedFileDataColumnMapping.geoLocationSeparator === separator.value">
|
||||
<v-btn value="latlon">{{ `${tt('Latitude')}${separator.value}${tt('Longitude')}` }}</v-btn>
|
||||
<v-btn value="lonlat">{{ `${tt('Longitude')}${separator.value}${tt('Latitude')}` }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-btn-group class="transaction-types-toggle" density="compact" variant="outlined"
|
||||
<v-btn-group class="toggle-buttons" density="compact" variant="outlined"
|
||||
divided v-if="parsedFileDataColumnMapping.geoLocationSeparator !== separator.value">
|
||||
<v-btn @click="parsedFileDataColumnMapping.setGeoLocationFormat(separator.value, 'latlon')">{{ `${tt('Latitude')}${separator.value}${tt('Longitude')}` }}</v-btn>
|
||||
<v-btn @click="parsedFileDataColumnMapping.setGeoLocationFormat(separator.value, 'lonlat')">{{ `${tt('Longitude')}${separator.value}${tt('Latitude')}` }}</v-btn>
|
||||
@@ -552,39 +552,3 @@ defineExpose({
|
||||
saveColumnMappingFile
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transaction-types-popup-menu .transaction-types-toggle {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle {
|
||||
height: auto !important;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
|
||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle button.v-btn {
|
||||
width: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,14 +40,27 @@
|
||||
</f7-list>
|
||||
|
||||
<f7-block class="combination-list-wrapper margin-vertical" key="default" v-show="!loading && hasAnyVisibleTag">
|
||||
<f7-list class="margin-top-half margin-bottom" strong inset dividers v-if="query['type'] === 'statisticsCurrent'">
|
||||
<f7-list class="margin-top-half margin-bottom" strong inset dividers>
|
||||
<f7-list-item radio
|
||||
:title="filterType.displayName"
|
||||
:value="filterType.type"
|
||||
:checked="tagFilterType === filterType.type"
|
||||
:title="tt(filterType.name)"
|
||||
:key="filterType.type"
|
||||
v-for="filterType in allTagFilterTypes"
|
||||
@change="tagFilterType = filterType.type">
|
||||
:value="filterType.type"
|
||||
:checked="includeTagFilterType === filterType.type"
|
||||
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
|
||||
@change="includeTagFilterType = filterType.type"
|
||||
v-if="includeTagsCount > 1">
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-list class="margin-top-half margin-bottom" strong inset dividers>
|
||||
<f7-list-item radio
|
||||
:title="tt(filterType.name)"
|
||||
:key="filterType.type"
|
||||
:value="filterType.type"
|
||||
:checked="excludeTagFilterType === filterType.type"
|
||||
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
|
||||
@change="excludeTagFilterType = filterType.type"
|
||||
v-if="excludeTagsCount > 1">
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
@@ -68,14 +81,15 @@
|
||||
</f7-block-title>
|
||||
<f7-accordion-content :style="{ height: collapseStates['default']!.opened ? 'auto' : '' }">
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content">
|
||||
<f7-list-item checkbox
|
||||
<f7-list-item link="#"
|
||||
popover-open=".tag-filter-state-popover-menu"
|
||||
:title="transactionTag.name"
|
||||
:value="transactionTag.id"
|
||||
:checked="!filterTagIds[transactionTag.id]"
|
||||
:key="transactionTag.id"
|
||||
:after="tt(filterTagIds[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : filterTagIds[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
|
||||
v-for="transactionTag in allTags"
|
||||
v-show="showHidden || !transactionTag.hidden"
|
||||
@change="updateTransactionTagSelected">
|
||||
@click="currentTransactionTagId = transactionTag.id">
|
||||
<template #media>
|
||||
<f7-icon class="transaction-tag-icon" f7="number">
|
||||
<f7-badge color="gray" class="right-bottom-icon" v-if="transactionTag.hidden">
|
||||
@@ -89,14 +103,35 @@
|
||||
</f7-accordion-item>
|
||||
</f7-block>
|
||||
|
||||
<f7-popover class="tag-filter-state-popover-menu"
|
||||
v-model:opened="showTagFilterStatePopover">
|
||||
<f7-list dividers>
|
||||
<f7-list-item :title="state.displayName"
|
||||
:class="{ 'list-item-selected': filterTagIds[currentTransactionTagId] === state.type }"
|
||||
:key="state.type"
|
||||
v-for="state in [
|
||||
{ type: TransactionTagFilterState.Include, displayName: tt('Included') },
|
||||
{ type: TransactionTagFilterState.Default, displayName: tt('Default') },
|
||||
{ type: TransactionTagFilterState.Exclude, displayName: tt('Excluded') }
|
||||
]"
|
||||
@click="updateCurrentTransactionTagState(state.type)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="filterTagIds[currentTransactionTagId] === state.type"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
|
||||
<f7-actions-group>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectAllTransactionTags">{{ tt('Select All') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectNoneTransactionTags">{{ tt('Select None') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectInvertTransactionTags">{{ tt('Invert Selection') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Include)">{{ tt('Set All to Included') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Default)">{{ tt('Set All to Default') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(false, TransactionTagFilterState.Exclude)">{{ tt('Set All to Excluded') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="selectAllVisibleTransactionTags">{{ tt('Select All Visible') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Include)">{{ tt('Set All Visible Items to Included') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Default)">{{ tt('Set All Visible Items to Default') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': !hasAnyVisibleTag }" @click="setAllToState(true, TransactionTagFilterState.Exclude)">{{ tt('Set All Visible Items to Excluded') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button v-if="!showHidden" @click="showHidden = true">{{ tt('Show Hidden Transaction Tags') }}</f7-actions-button>
|
||||
@@ -115,16 +150,14 @@ import type { Router } from 'framework7/types';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useI18nUIComponents } from '@/lib/ui/mobile.ts';
|
||||
import { useTransactionTagFilterSettingPageBase } from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts';
|
||||
import {
|
||||
useTransactionTagFilterSettingPageBase,
|
||||
TransactionTagFilterState
|
||||
} from '@/views/base/settings/TransactionTagFilterSettingPageBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import {
|
||||
selectAllVisible,
|
||||
selectAll,
|
||||
selectNone,
|
||||
selectInvert
|
||||
} from '@/lib/common.ts';
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
|
||||
interface CollapseState {
|
||||
opened: boolean;
|
||||
@@ -144,11 +177,13 @@ const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterTagIds,
|
||||
tagFilterType,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
title,
|
||||
applyText,
|
||||
allTags,
|
||||
allTagFilterTypes,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
loadFilterTagIds,
|
||||
@@ -158,6 +193,8 @@ const {
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const currentTransactionTagId = ref<string>('');
|
||||
const showTagFilterStatePopover = ref<boolean>(false);
|
||||
const showMoreActionSheet = ref<boolean>(false);
|
||||
|
||||
const collapseStates = ref<Record<string, CollapseState>>({
|
||||
@@ -186,32 +223,20 @@ function init(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function updateTransactionTagSelected(e: Event): void {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const transactionTagId = target.value;
|
||||
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
|
||||
function updateCurrentTransactionTagState(state: number): void {
|
||||
filterTagIds.value[currentTransactionTagId.value] = state;
|
||||
showTagFilterStatePopover.value = false;
|
||||
currentTransactionTagId.value = '';
|
||||
}
|
||||
|
||||
if (!transactionTag) {
|
||||
return;
|
||||
function setAllToState(onlyVisible: boolean, value: TransactionTagFilterState): void {
|
||||
for (const tag of allTags.value) {
|
||||
if (onlyVisible && !showHidden.value && tag.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filterTagIds.value[tag.id] = value;
|
||||
}
|
||||
|
||||
filterTagIds.value[transactionTag.id] = !target.checked;
|
||||
}
|
||||
|
||||
function selectAllTransactionTags(): void {
|
||||
selectAll(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
}
|
||||
|
||||
function selectNoneTransactionTags(): void {
|
||||
selectNone(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
}
|
||||
|
||||
function selectInvertTransactionTags(): void {
|
||||
selectInvert(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
}
|
||||
|
||||
function selectAllVisibleTransactionTags(): void {
|
||||
selectAllVisible(filterTagIds.value, transactionTagsStore.allTransactionTagsMap);
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<span :class="{ 'tabbar-item-changed': query.accountIds }">{{ queryAccountName }}</span>
|
||||
</f7-link>
|
||||
<f7-link popover-open=".more-popover-menu">
|
||||
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter || query.tagIds }"></f7-icon>
|
||||
<f7-icon f7="ellipsis_vertical" :class="{ 'tabbar-item-changed': query.type > 0 || query.amountFilter || query.tagFilter }"></f7-icon>
|
||||
</f7-link>
|
||||
</f7-toolbar>
|
||||
|
||||
@@ -509,52 +509,37 @@
|
||||
<f7-list-item group-title>
|
||||
<small>{{ tt('Tags') }}</small>
|
||||
</f7-list-item>
|
||||
<f7-list-item :class="{ 'list-item-selected': !query.tagIds }" :title="tt('All')" @click="changeTagFilter('')">
|
||||
<f7-list-item :class="{ 'list-item-selected': !query.tagFilter }" :title="tt('All')" @click="changeTagFilter('')">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.tagIds"></f7-icon>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="!query.tagFilter"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
<f7-list-item :class="{ 'list-item-selected': query.tagIds === 'none' }" :title="tt('Without Tags')" @click="changeTagFilter('none')">
|
||||
<f7-list-item :class="{ 'list-item-selected': query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue }" :title="tt('Without Tags')" @click="changeTagFilter(TransactionTagFilter.TransactionNoTagFilterValue)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagIds === 'none'"></f7-icon>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter === TransactionTagFilter.TransactionNoTagFilterValue"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
<f7-list-item :class="{ 'list-item-selected': query.tagIds && queryAllFilterTagIdsCount > 1 }"
|
||||
<f7-list-item :class="{ 'list-item-selected': query.tagFilter && queryAllFilterTagIdsCount > 1 }"
|
||||
:title="tt('Multiple Tags')" @click="filterMultipleTags()" v-if="allAvailableTagsCount > 0">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagIds && queryAllFilterTagIdsCount > 1"></f7-icon>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.tagFilter && queryAllFilterTagIdsCount > 1"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
|
||||
<template v-if="query.tagIds && query.tagIds !== 'none'">
|
||||
<f7-list-item :title="filterType.displayName"
|
||||
:key="filterType.type"
|
||||
v-for="filterType in allTransactionTagFilterTypes"
|
||||
@click="changeTagFilterType(filterType.type)"
|
||||
>
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon"
|
||||
f7="checkmark_alt"
|
||||
v-if="query.tagFilterType === filterType.type">
|
||||
</f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</template>
|
||||
|
||||
<f7-list-item :title="transactionTag.name"
|
||||
:class="{ 'list-item-selected': query.tagIds === transactionTag.id, 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && queryAllFilterTagIds[transactionTag.id] }"
|
||||
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
|
||||
:key="transactionTag.id"
|
||||
v-for="transactionTag in allTransactionTags"
|
||||
v-show="!transactionTag.hidden || query.tagIds === transactionTag.id"
|
||||
@click="changeTagFilter(transactionTag.id)"
|
||||
v-show="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])"
|
||||
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())"
|
||||
>
|
||||
<template #before-title>
|
||||
<f7-icon class="transaction-tag-name transaction-tag-icon" f7="number"></f7-icon>
|
||||
</template>
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon"
|
||||
f7="checkmark_alt"
|
||||
v-if="query.tagIds === transactionTag.id">
|
||||
:f7="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
|
||||
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
</f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
@@ -597,7 +582,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
import { type TransactionMonthList, useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { type TypeAndDisplayName, keys } from '@/core/base.ts';
|
||||
import { keys } from '@/core/base.ts';
|
||||
import { TextDirection } from '@/core/text.ts';
|
||||
import {
|
||||
type TextualYearMonth,
|
||||
@@ -609,10 +594,12 @@ import {
|
||||
import { AmountFilterType } from '@/core/numeral.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import type { Transaction } from '@/models/transaction.ts';
|
||||
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
||||
|
||||
import {
|
||||
isNumber
|
||||
isDefined,
|
||||
isNumber,
|
||||
objectFieldWithValueToArrayItem
|
||||
} from '@/lib/common.ts';
|
||||
import {
|
||||
getCurrentUnixTime,
|
||||
@@ -644,7 +631,6 @@ const props = defineProps<{
|
||||
const {
|
||||
tt,
|
||||
getCurrentLanguageTextDirection,
|
||||
getAllTransactionTagFilterTypes,
|
||||
getWeekdayShortName,
|
||||
getCalendarDisplayDayOfMonthFromUnixTime
|
||||
} = useI18n();
|
||||
@@ -723,8 +709,6 @@ const showDeleteActionSheet = ref<boolean>(false);
|
||||
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
|
||||
const isDarkMode = computed<boolean>(() => environmentsStore.framework7DarkMode || false);
|
||||
|
||||
const allTransactionTagFilterTypes = computed<TypeAndDisplayName[]>(() => getAllTransactionTagFilterTypes());
|
||||
|
||||
const transactions = computed<TransactionMonthList[]>(() => {
|
||||
if (loading.value) {
|
||||
return [];
|
||||
@@ -925,8 +909,7 @@ function init(): void {
|
||||
type: initQuery['type'] && parseInt(initQuery['type']) > 0 ? parseInt(initQuery['type']) : undefined,
|
||||
categoryIds: initQuery['categoryIds'],
|
||||
accountIds: initQuery['accountIds'],
|
||||
tagIds: initQuery['tagIds'],
|
||||
tagFilterType: initQuery['tagFilterType'] && parseInt(initQuery['tagFilterType']) >= 0 ? parseInt(initQuery['tagFilterType']) : undefined,
|
||||
tagFilter: initQuery['tagFilter'],
|
||||
keyword: initQuery['keyword']
|
||||
});
|
||||
|
||||
@@ -1277,13 +1260,13 @@ function filterMultipleAccounts(): void {
|
||||
props.f7router.navigate('/settings/filter/account?type=transactionListCurrent');
|
||||
}
|
||||
|
||||
function changeTagFilter(tagIds: string): void {
|
||||
if (query.value.tagIds === tagIds) {
|
||||
function changeTagFilter(tagFilter: string): void {
|
||||
if (query.value.tagFilter === tagFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changed = transactionsStore.updateTransactionListFilter({
|
||||
tagIds: tagIds
|
||||
tagFilter: tagFilter
|
||||
});
|
||||
|
||||
showMorePopover.value = false;
|
||||
@@ -1297,22 +1280,6 @@ function filterMultipleTags(): void {
|
||||
props.f7router.navigate('/settings/filter/tag?type=transactionListCurrent');
|
||||
}
|
||||
|
||||
function changeTagFilterType(filterType: number): void {
|
||||
if (query.value.tagFilterType === filterType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changed = transactionsStore.updateTransactionListFilter({
|
||||
tagFilterType: filterType
|
||||
});
|
||||
|
||||
showMorePopover.value = false;
|
||||
|
||||
if (changed) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
function changeKeywordFilter(keyword: string): void {
|
||||
if (query.value.keyword === keyword) {
|
||||
return;
|
||||
@@ -1383,8 +1350,8 @@ function add(): void {
|
||||
params.push(`accountId=${query.value.accountIds}`);
|
||||
}
|
||||
|
||||
if (query.value.tagIds) {
|
||||
params.push(`tagIds=${query.value.tagIds}`);
|
||||
if (query.value.tagFilter) {
|
||||
params.push(`tagIds=${objectFieldWithValueToArrayItem(queryAllFilterTagIds.value, true).join(',') || ''}`);
|
||||
}
|
||||
|
||||
props.f7router.navigate(`/transaction/add?${params.join('&')}`);
|
||||
|
||||
Reference in New Issue
Block a user