mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 23:17:33 +08:00
support transaction tag group
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
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 { entries, values } from '@/core/base.ts';
|
||||
import { entries, keys, values } from '@/core/base.ts';
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { TransactionTagFilter } from '@/models/transaction.ts';
|
||||
|
||||
import { objectFieldWithValueToArrayItem } from '@/lib/common.ts';
|
||||
import { objectFieldToArrayItem } from '@/lib/common.ts';
|
||||
|
||||
export enum TransactionTagFilterState {
|
||||
Default = 0,
|
||||
@@ -17,7 +22,27 @@ export enum TransactionTagFilterState {
|
||||
Exclude = 2
|
||||
}
|
||||
|
||||
interface TransactionGroupTagFilterTypes {
|
||||
includeType: number;
|
||||
excludeType: number;
|
||||
}
|
||||
|
||||
function getEmptyGroupTagFilterTypesMap(allTransactionTagsByGroupMap: Record<string, TransactionTag[]>): Record<string, TransactionGroupTagFilterTypes> {
|
||||
const ret: Record<string, TransactionGroupTagFilterTypes> = {};
|
||||
|
||||
for (const groupId of keys(allTransactionTagsByGroupMap)) {
|
||||
ret[groupId] = {
|
||||
includeType: TransactionTagFilterType.HasAny.type,
|
||||
excludeType: TransactionTagFilterType.NotHasAny.type
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
const { tt } = useI18n();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
const statisticsStore = useStatisticsStore();
|
||||
@@ -25,12 +50,11 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
const loading = ref<boolean>(true);
|
||||
const showHidden = ref<boolean>(false);
|
||||
const filterContent = ref<string>('');
|
||||
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 tagFilterStateMap = ref<Record<string, TransactionTagFilterState>>({});
|
||||
const groupTagFilterTypesMap = ref<Record<string, TransactionGroupTagFilterTypes>>(getEmptyGroupTagFilterTypesMap(transactionTagsStore.allTransactionTagsByGroupMap));
|
||||
|
||||
const lowerCaseFilterContent = computed<string>(() => filterContent.value.toLowerCase());
|
||||
|
||||
const title = computed<string>(() => {
|
||||
return 'Filter Transaction Tags';
|
||||
@@ -40,23 +64,85 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
return 'Apply';
|
||||
});
|
||||
|
||||
const allVisibleTags = computed<TransactionTag[]>(() => {
|
||||
const ret: TransactionTag[] = [];
|
||||
const allTags = showHidden.value ? transactionTagsStore.allTransactionTags : transactionTagsStore.allVisibleTags;
|
||||
const lowercaseFilterContent = filterContent.value ? filterContent.value.toLowerCase() : '';
|
||||
const groupTagFilterStateCountMap = computed<Record<string, Record<TransactionTagFilterState, number>>>(() => {
|
||||
const ret: Record<string, Record<TransactionTagFilterState, number>> = {};
|
||||
|
||||
for (const tag of allTags) {
|
||||
if (lowercaseFilterContent && !tag.name.toLowerCase().includes(lowercaseFilterContent)) {
|
||||
continue;
|
||||
for (const [groupId, tags] of entries(transactionTagsStore.allTransactionTagsByGroupMap)) {
|
||||
const stateCountMap: Record<TransactionTagFilterState, number> = {
|
||||
[TransactionTagFilterState.Default]: 0,
|
||||
[TransactionTagFilterState.Include]: 0,
|
||||
[TransactionTagFilterState.Exclude]: 0
|
||||
};
|
||||
|
||||
for (const tag of tags) {
|
||||
const state = tagFilterStateMap.value[tag.id] ?? TransactionTagFilterState.Default;
|
||||
stateCountMap[state] = (stateCountMap[state] || 0) + 1;
|
||||
}
|
||||
|
||||
ret.push(tag);
|
||||
ret[groupId] = stateCountMap;
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
const allTagGroupsWithDefault = computed<TransactionTagGroup[]>(() => {
|
||||
const allGroups: TransactionTagGroup[] = [];
|
||||
const tagsInDefaultGroup = transactionTagsStore.allTransactionTagsByGroupMap[DEFAULT_TAG_GROUP_ID];
|
||||
|
||||
if (tagsInDefaultGroup && tagsInDefaultGroup.length) {
|
||||
const defaultGroup = TransactionTagGroup.createNewTagGroup(tt('Default Group'));
|
||||
defaultGroup.id = DEFAULT_TAG_GROUP_ID;
|
||||
allGroups.push(defaultGroup);
|
||||
}
|
||||
|
||||
for (const tagGroup of transactionTagsStore.allTransactionTagGroups) {
|
||||
const tagsInGroup = transactionTagsStore.allTransactionTagsByGroupMap[tagGroup.id];
|
||||
|
||||
if (tagsInGroup && tagsInGroup.length) {
|
||||
allGroups.push(tagGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return allGroups;
|
||||
});
|
||||
|
||||
const allVisibleTags = computed<Record<string, TransactionTag[]>>(() => {
|
||||
const ret: Record<string, TransactionTag[]> = {};
|
||||
const allTagGroups = transactionTagsStore.allTransactionTagsByGroupMap;
|
||||
|
||||
for (const [groupId, tags] of entries(allTagGroups)) {
|
||||
const visibleTags: TransactionTag[] = [];
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!showHidden.value && tag.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lowerCaseFilterContent.value && !tag.name.toLowerCase().includes(lowerCaseFilterContent.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visibleTags.push(tag);
|
||||
}
|
||||
|
||||
if (visibleTags.length > 0) {
|
||||
ret[groupId] = visibleTags;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
const allVisibleTagGroupIds = computed<string[]>(() => objectFieldToArrayItem(allVisibleTags.value));
|
||||
const hasAnyAvailableTag = computed<boolean>(() => transactionTagsStore.allAvailableTagsCount > 0);
|
||||
const hasAnyVisibleTag = computed<boolean>(() => allVisibleTags.value.length > 0);
|
||||
const hasAnyVisibleTag = computed<boolean>(() => {
|
||||
for (const tags of values(allVisibleTags.value)) {
|
||||
if (tags.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
function loadFilterTagIds(): boolean {
|
||||
let tagFilters: TransactionTagFilter[] = [];
|
||||
@@ -70,6 +156,7 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
}
|
||||
|
||||
const allTagIdsMap: Record<string, TransactionTagFilterState> = {};
|
||||
const allGroupTagFilterTypesMap: Record<string, TransactionGroupTagFilterTypes> = getEmptyGroupTagFilterTypesMap(transactionTagsStore.allTransactionTagsByGroupMap);
|
||||
|
||||
for (const transactionTag of values(transactionTagsStore.allTransactionTagsMap)) {
|
||||
allTagIdsMap[transactionTag.id] = TransactionTagFilterState.Default;
|
||||
@@ -80,50 +167,69 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
|
||||
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;
|
||||
const tag = transactionTagsStore.allTransactionTagsMap[tagId];
|
||||
|
||||
if (!tag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupFilterTypes = allGroupTagFilterTypesMap[tag.groupId];
|
||||
|
||||
if (groupFilterTypes) {
|
||||
if (state === TransactionTagFilterState.Include) {
|
||||
groupFilterTypes.includeType = tagFilter.type.type;
|
||||
} else if (state === TransactionTagFilterState.Exclude) {
|
||||
groupFilterTypes.excludeType = tagFilter.type.type;
|
||||
}
|
||||
|
||||
allTagIdsMap[tagId] = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterTagIds.value = allTagIdsMap;
|
||||
tagFilterStateMap.value = allTagIdsMap;
|
||||
groupTagFilterTypesMap.value = allGroupTagFilterTypesMap;
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveFilterTagIds(): boolean {
|
||||
const includeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(includeTagFilterType.value) ?? TransactionTagFilterType.HasAny);
|
||||
const excludeTagFilter: TransactionTagFilter = TransactionTagFilter.create(TransactionTagFilterType.parse(excludeTagFilterType.value) ?? TransactionTagFilterType.NotHasAny);
|
||||
const tagFilters: TransactionTagFilter[] = [];
|
||||
let changed = true;
|
||||
|
||||
for (const [transactionTagId, state] of entries(filterTagIds.value)) {
|
||||
const transactionTag = transactionTagsStore.allTransactionTagsMap[transactionTagId];
|
||||
for (const [groupId, tags] of entries(transactionTagsStore.allTransactionTagsByGroupMap)) {
|
||||
const groupFilterTypes = groupTagFilterTypesMap.value[groupId];
|
||||
|
||||
if (!transactionTag) {
|
||||
continue;
|
||||
if (groupFilterTypes && tags && tags.length > 0) {
|
||||
const includeTagIds: string[] = [];
|
||||
const excludeTagIds: string[] = [];
|
||||
|
||||
for (const tag of tags) {
|
||||
const state = tagFilterStateMap.value[tag.id] ?? TransactionTagFilterState.Default;
|
||||
|
||||
if (state === TransactionTagFilterState.Include) {
|
||||
includeTagIds.push(tag.id);
|
||||
} else if (state === TransactionTagFilterState.Exclude) {
|
||||
excludeTagIds.push(tag.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeTagIds.length > 0) {
|
||||
const includeTagFilter = TransactionTagFilter.create(includeTagIds, TransactionTagFilterType.parse(groupFilterTypes.includeType) ?? TransactionTagFilterType.HasAny);
|
||||
tagFilters.push(includeTagFilter);
|
||||
}
|
||||
|
||||
if (excludeTagIds.length > 0) {
|
||||
const excludeTagFilter = TransactionTagFilter.create(excludeTagIds, TransactionTagFilterType.parse(groupFilterTypes.excludeType) ?? TransactionTagFilterType.NotHasAny);
|
||||
tagFilters.push(excludeTagFilter);
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
@@ -152,15 +258,15 @@ export function useTransactionTagFilterSettingPageBase(type?: string) {
|
||||
loading,
|
||||
showHidden,
|
||||
filterContent,
|
||||
filterTagIds,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
tagFilterStateMap,
|
||||
groupTagFilterTypesMap,
|
||||
// computed states
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
title,
|
||||
applyText,
|
||||
groupTagFilterStateCountMap,
|
||||
allTagGroupsWithDefault,
|
||||
allVisibleTags,
|
||||
allVisibleTagGroupIds,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
// functions
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import { isNoAvailableTag } from '@/lib/tag.ts';
|
||||
|
||||
export function useTagListPageBase() {
|
||||
const { tt } = useI18n();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const activeTagGroupId = ref<string>(DEFAULT_TAG_GROUP_ID);
|
||||
const newTag = ref<TransactionTag | null>(null);
|
||||
const editingTag = ref<TransactionTag>(TransactionTag.createNewTag());
|
||||
const loading = ref<boolean>(true);
|
||||
const showHidden = ref<boolean>(false);
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
|
||||
const allTagGroupsWithDefault = computed<TransactionTagGroup[]>(() => {
|
||||
const allGroups: TransactionTagGroup[] = [];
|
||||
const defaultGroup = TransactionTagGroup.createNewTagGroup(tt('Default Group'));
|
||||
defaultGroup.id = DEFAULT_TAG_GROUP_ID;
|
||||
allGroups.push(defaultGroup);
|
||||
allGroups.push(...transactionTagsStore.allTransactionTagGroups);
|
||||
return allGroups;
|
||||
});
|
||||
const tags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTagsByGroupMap[activeTagGroupId.value] || []);
|
||||
|
||||
const noAvailableTag = computed<boolean>(() => isNoAvailableTag(tags.value, showHidden.value));
|
||||
const hasEditingTag = computed<boolean>(() => !!(newTag.value || (editingTag.value.id && editingTag.value.id !== '')));
|
||||
|
||||
function isTagModified(tag: TransactionTag): boolean {
|
||||
if (tag.id) {
|
||||
return editingTag.value.name !== '' && editingTag.value.name !== tag.name;
|
||||
} else {
|
||||
return tag.name !== '';
|
||||
}
|
||||
}
|
||||
|
||||
function switchTagGroup(tagGroupId: string): void {
|
||||
activeTagGroupId.value = tagGroupId;
|
||||
|
||||
if (newTag.value) {
|
||||
newTag.value.groupId = tagGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
newTag.value = TransactionTag.createNewTag('', activeTagGroupId.value);
|
||||
}
|
||||
|
||||
function edit(tag: TransactionTag): void {
|
||||
editingTag.value.id = tag.id;
|
||||
editingTag.value.groupId = tag.groupId;
|
||||
editingTag.value.name = tag.name;
|
||||
}
|
||||
|
||||
return {
|
||||
// states
|
||||
activeTagGroupId,
|
||||
newTag,
|
||||
editingTag,
|
||||
loading,
|
||||
showHidden,
|
||||
displayOrderModified,
|
||||
// computed states
|
||||
allTagGroupsWithDefault,
|
||||
tags,
|
||||
noAvailableTag,
|
||||
hasEditingTag,
|
||||
// functions
|
||||
isTagModified,
|
||||
switchTagGroup,
|
||||
add,
|
||||
edit
|
||||
};
|
||||
}
|
||||
@@ -109,7 +109,6 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo
|
||||
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value, customAccountCategoryOrder.value));
|
||||
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
|
||||
const allCategoriesMap = computed<Record<string, TransactionCategory>>(() => transactionCategoriesStore.allTransactionCategoriesMap);
|
||||
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
const allTagsMap = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
|
||||
const firstVisibleAccountId = computed<string | undefined>(() => allVisibleAccounts.value && allVisibleAccounts.value[0] ? allVisibleAccounts.value[0].id : undefined);
|
||||
|
||||
@@ -452,7 +451,6 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo
|
||||
allVisibleCategorizedAccounts,
|
||||
allCategories,
|
||||
allCategoriesMap,
|
||||
allTags,
|
||||
allTagsMap,
|
||||
firstVisibleAccountId,
|
||||
hasVisibleExpenseCategories,
|
||||
|
||||
@@ -15,9 +15,11 @@ import { type TextualYearMonthDay, type Year0BasedMonth, type LocalizedDateRange
|
||||
import { AccountType } from '@/core/account.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import { DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts';
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import type { Account } from '@/models/account.ts';
|
||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
||||
|
||||
@@ -136,8 +138,16 @@ export function useTransactionListPageBase() {
|
||||
}
|
||||
|
||||
return totalCount;
|
||||
|
||||
});
|
||||
const allTransactionTagGroupsWithDefault = computed<TransactionTagGroup[]>(() => {
|
||||
const allGroups: TransactionTagGroup[] = [];
|
||||
const defaultGroup = TransactionTagGroup.createNewTagGroup(tt('Default Group'));
|
||||
defaultGroup.id = DEFAULT_TAG_GROUP_ID;
|
||||
allGroups.push(defaultGroup);
|
||||
allGroups.push(...transactionTagsStore.allTransactionTagGroups);
|
||||
return allGroups;
|
||||
});
|
||||
const allTransactionTagsByGroup = computed<Record<string, TransactionTag[]>>(() => transactionTagsStore.allTransactionTagsByGroupMap);
|
||||
const allTransactionTags = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
|
||||
const allAvailableTagsCount = computed<number>(() => transactionTagsStore.allAvailableTagsCount);
|
||||
|
||||
@@ -282,6 +292,22 @@ export function useTransactionListPageBase() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasVisibleTagsInTagGroup(tagGroup: TransactionTagGroup): boolean {
|
||||
const tagsInGroup = allTransactionTagsByGroup.value[tagGroup.id];
|
||||
|
||||
if (!tagsInGroup || !tagsInGroup.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const tag of tagsInGroup) {
|
||||
if (!tag.hidden || queryAllFilterTagIds.value[tag.id]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSameAsDefaultTimezoneOffsetMinutes(transaction: Transaction): boolean {
|
||||
return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time);
|
||||
}
|
||||
@@ -391,6 +417,8 @@ export function useTransactionListPageBase() {
|
||||
allCategories,
|
||||
allPrimaryCategories,
|
||||
allAvailableCategoriesCount,
|
||||
allTransactionTagGroupsWithDefault,
|
||||
allTransactionTagsByGroup,
|
||||
allTransactionTags,
|
||||
allAvailableTagsCount,
|
||||
displayPageTypeName,
|
||||
@@ -416,6 +444,7 @@ export function useTransactionListPageBase() {
|
||||
canAddTransaction,
|
||||
// functions
|
||||
hasSubCategoryInQuery,
|
||||
hasVisibleTagsInTagGroup,
|
||||
isSameAsDefaultTimezoneOffsetMinutes,
|
||||
getDisplayTime,
|
||||
getDisplayLongDate,
|
||||
|
||||
@@ -55,60 +55,69 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text :class="{ 'flex-grow-1 overflow-y-auto': dialogMode }" v-else-if="!loading && hasAnyVisibleTag">
|
||||
<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">
|
||||
<v-expansion-panel class="border" key="default" value="default">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ms-3">{{ tt('Tags') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allVisibleTags">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<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>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panels class="tag-categories" multiple v-model="expandTagGroups">
|
||||
<template :key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault">
|
||||
<v-expansion-panel class="border" :value="tagGroup.id" v-if="allVisibleTags[tagGroup.id] && allVisibleTags[tagGroup.id]!.length > 0">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ms-3 text-truncate">{{ tagGroup.name }}</span>
|
||||
<v-spacer/>
|
||||
<div class="d-flex me-3" v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]">
|
||||
<v-btn color="secondary" density="compact" variant="outlined"
|
||||
v-if="groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] > 1">
|
||||
{{ groupTagFilterTypesMap[tagGroup.id]!.includeType === TransactionTagFilterType.HasAll.type ? tt(TransactionTagFilterType.HasAll.name) : tt(TransactionTagFilterType.HasAny.name) }}
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :key="filterType.type" :title="tt(filterType.name)"
|
||||
:append-icon="groupTagFilterTypesMap[tagGroup.id]!.includeType === filterType.type ? mdiCheck : undefined"
|
||||
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
|
||||
@click="updateTransactionTagGroupIncludeType(tagGroup, filterType)"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn class="ms-2" color="secondary" density="compact" variant="outlined"
|
||||
v-if="groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] > 1">
|
||||
{{ groupTagFilterTypesMap[tagGroup.id]!.excludeType === TransactionTagFilterType.NotHasAll.type ? tt(TransactionTagFilterType.NotHasAll.name) : tt(TransactionTagFilterType.NotHasAny.name) }}
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :key="filterType.type" :title="tt(filterType.name)"
|
||||
:append-icon="groupTagFilterTypesMap[tagGroup.id]!.excludeType === filterType.type ? mdiCheck : undefined"
|
||||
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
|
||||
@click="updateTransactionTagGroupExcludeType(tagGroup, filterType)"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allVisibleTags[tagGroup.id]">
|
||||
<v-list-item class="ps-2">
|
||||
<template #prepend>
|
||||
<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="tagFilterStateMap[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>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
@@ -136,11 +145,15 @@ import {
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { values } from '@/core/base.ts';
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
|
||||
import type { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
mdiMagnify,
|
||||
mdiCheck,
|
||||
mdiSelectAll,
|
||||
mdiEyeOutline,
|
||||
mdiEyeOffOutline,
|
||||
@@ -166,14 +179,14 @@ const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterContent,
|
||||
filterTagIds,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
tagFilterStateMap,
|
||||
groupTagFilterTypesMap,
|
||||
title,
|
||||
applyText,
|
||||
groupTagFilterStateCountMap,
|
||||
allTagGroupsWithDefault,
|
||||
allVisibleTags,
|
||||
allVisibleTagGroupIds,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
loadFilterTagIds,
|
||||
@@ -184,13 +197,14 @@ const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const expandTagCategories = ref<string[]>([ 'default' ]);
|
||||
const expandTagGroups = ref<string[]>(allVisibleTagGroupIds.value);
|
||||
|
||||
function init(): void {
|
||||
transactionTagsStore.loadAllTags({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
expandTagGroups.value = allVisibleTagGroupIds.value;
|
||||
|
||||
if (!loadFilterTagIds()) {
|
||||
snackbar.value?.showError('Parameter Invalid');
|
||||
@@ -205,23 +219,35 @@ function init(): void {
|
||||
}
|
||||
|
||||
function updateTransactionTagState(transactionTag: TransactionTag, value: TransactionTagFilterState): void {
|
||||
filterTagIds.value[transactionTag.id] = value;
|
||||
tagFilterStateMap.value[transactionTag.id] = value;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTagIncludeType(value: number): void {
|
||||
includeTagFilterType.value = value;
|
||||
function updateTransactionTagGroupIncludeType(tagGroup: TransactionTagGroup, filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[tagGroup.id];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.includeType = filterType.type;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTagExcludeType(value: number): void {
|
||||
excludeTagFilterType.value = value;
|
||||
function updateTransactionTagGroupExcludeType(tagGroup: TransactionTagGroup, filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[tagGroup.id];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.excludeType = filterType.type;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
@@ -229,8 +255,10 @@ function updateTransactionTagExcludeType(value: number): void {
|
||||
}
|
||||
|
||||
function setAllTagsState(value: TransactionTagFilterState): void {
|
||||
for (const tag of allVisibleTags.value) {
|
||||
filterTagIds.value[tag.id] = value;
|
||||
for (const tags of values(allVisibleTags.value)) {
|
||||
for (const tag of tags) {
|
||||
tagFilterStateMap.value[tag.id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.autoSave) {
|
||||
|
||||
@@ -171,22 +171,24 @@
|
||||
@error="onShowDateRangeError" />
|
||||
|
||||
<explorer-list-dialog ref="explorerListDialog" />
|
||||
<explorer-rename-dialog ref="explorerRenameDialog" />
|
||||
<edit-dialog ref="editDialog" :type="TransactionEditPageType.Transaction" />
|
||||
<export-dialog ref="exportDialog" />
|
||||
|
||||
<rename-dialog ref="renameDialog"
|
||||
:default-title="tt('Rename Explorer')"
|
||||
:label="tt('Explorer Name')" :placeholder="tt('Explorer Name')" />
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RenameDialog from '@/components/desktop/RenameDialog.vue';
|
||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
import ExplorerQueryTab from '@/views/desktop/insights/tabs/ExplorerQueryTab.vue';
|
||||
import ExplorerDataTableTab from '@/views/desktop/insights/tabs/ExplorerDataTableTab.vue';
|
||||
import ExplorerChartTab from '@/views/desktop/insights/tabs/ExplorerChartTab.vue';
|
||||
import ExplorerListDialog from '@/views/desktop/insights/dialogs/ExplorerListDialog.vue';
|
||||
import ExplorerRenameDialog from '@/views/desktop/insights/dialogs/ExplorerRenameDialog.vue';
|
||||
import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue';
|
||||
import ExportDialog from '@/views/desktop/statistics/transaction/dialogs/ExportDialog.vue';
|
||||
|
||||
@@ -251,12 +253,12 @@ const props = defineProps<InsightsExplorerProps>();
|
||||
|
||||
type ExplorerPageTabType = 'query' | 'table' | 'chart';
|
||||
|
||||
type RenameDialogType = InstanceType<typeof RenameDialog>;
|
||||
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
type ExplorerDataTableTabType = InstanceType<typeof ExplorerDataTableTab>;
|
||||
type ExplorerChartTabType = InstanceType<typeof ExplorerChartTab>;
|
||||
type ExplorerListDialogType = InstanceType<typeof ExplorerListDialog>;
|
||||
type ExplorerRenameDialogType = InstanceType<typeof ExplorerRenameDialog>;
|
||||
type EditDialogType = InstanceType<typeof EditDialog>;
|
||||
type ExportDialogType = InstanceType<typeof ExportDialog>;
|
||||
|
||||
@@ -282,12 +284,12 @@ const timezoneTypeIconMap = {
|
||||
[TimezoneTypeForStatistics.TransactionTimezone.type]: mdiInvoiceTextClockOutline
|
||||
};
|
||||
|
||||
const renameDialog = useTemplateRef<RenameDialogType>('renameDialog');
|
||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const explorerDataTableTab = useTemplateRef<ExplorerDataTableTabType>('explorerDataTableTab');
|
||||
const explorerChartTab = useTemplateRef<ExplorerChartTabType>('explorerChartTab');
|
||||
const explorerListDialog = useTemplateRef<ExplorerListDialogType>('explorerListDialog');
|
||||
const explorerRenameDialog = useTemplateRef<ExplorerRenameDialogType>('explorerRenameDialog');
|
||||
const exportDialog = useTemplateRef<ExportDialogType>('exportDialog');
|
||||
const editDialog = useTemplateRef<EditDialogType>('editDialog');
|
||||
|
||||
@@ -515,7 +517,7 @@ function showChangeExplorerDisplayOrderDialog(): void {
|
||||
|
||||
function saveExplorer(saveAs?: boolean): void {
|
||||
if (saveAs || !currentExplorer.value.name) {
|
||||
explorerRenameDialog.value?.open(currentExplorer.value.name || '', tt('Set Explorer Name')).then((newName: string) => {
|
||||
renameDialog.value?.open(currentExplorer.value.name || '', tt('Set Explorer Name')).then((newName: string) => {
|
||||
currentExplorer.value.name = newName;
|
||||
doSaveExplorer(saveAs);
|
||||
})
|
||||
@@ -571,7 +573,7 @@ function restoreExplorer(): void {
|
||||
}
|
||||
|
||||
function setExplorerName(): void {
|
||||
explorerRenameDialog.value?.open(currentExplorer.value.name || '').then((newName: string) => {
|
||||
renameDialog.value?.open(currentExplorer.value.name || '').then((newName: string) => {
|
||||
currentExplorer.value.name = newName;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<v-dialog max-width="500" :persistent="oldExplorerName !== newExplorerName" v-model="showState">
|
||||
<v-card class="pa-sm-1 pa-md-2">
|
||||
<template #title>
|
||||
<h4 class="text-h4 text-wrap">{{ dialogTitle || tt('Rename Explorer') }}</h4>
|
||||
</template>
|
||||
<v-card-text class="w-100 d-flex justify-center">
|
||||
<v-text-field persistent-placeholder
|
||||
:autofocus="true"
|
||||
:label="tt('Explorer Name')"
|
||||
:placeholder="tt('Explorer Name')"
|
||||
v-model="newExplorerName"
|
||||
@keyup.enter="save" />
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
|
||||
<v-btn color="primary" :disabled="!newExplorerName || oldExplorerName === newExplorerName" @click="save">
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" @click="cancel">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
let resolveFunc: ((name: string) => void) | null = null;
|
||||
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||
|
||||
const showState = ref<boolean>(false);
|
||||
const dialogTitle = ref<string | undefined>(undefined);
|
||||
const oldExplorerName = ref<string>('');
|
||||
const newExplorerName = ref<string>('');
|
||||
|
||||
function open(currentExplorerName: string, title?: string): Promise<string> {
|
||||
showState.value = true;
|
||||
dialogTitle.value = title;
|
||||
oldExplorerName.value = currentExplorerName;
|
||||
newExplorerName.value = currentExplorerName;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveFunc = resolve;
|
||||
rejectFunc = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
if (!newExplorerName.value || oldExplorerName.value === newExplorerName.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolveFunc?.(newExplorerName.value);
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
rejectFunc?.();
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -251,56 +251,12 @@
|
||||
(conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.IsEmpty.value || conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.IsNotEmpty.value)"
|
||||
/>
|
||||
|
||||
<v-autocomplete
|
||||
<transaction-tag-auto-complete
|
||||
density="compact"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
auto-select-first
|
||||
persistent-placeholder
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
:disabled="loading || disabled || !!editingQuery"
|
||||
:placeholder="tt('None')"
|
||||
:items="allTags"
|
||||
v-model="conditionWithRelation.condition.value"
|
||||
v-model:search="tagSearchContent"
|
||||
v-else-if="conditionWithRelation.condition.operator !== TransactionExplorerConditionOperator.IsEmpty.value && conditionWithRelation.condition.operator !== TransactionExplorerConditionOperator.IsNotEmpty.value"
|
||||
>
|
||||
<template #chip="{ props, item }">
|
||||
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
|
||||
</template>
|
||||
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item :disabled="true" v-bind="props"
|
||||
v-if="item.raw.hidden && item.raw.name.toLowerCase().indexOf(tagSearchContent.toLowerCase()) >= 0 && isAllFilteredTagHidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<v-list class="py-0">
|
||||
<v-list-item>{{ tt('No available tag') }}</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-text-field disabled density="compact"
|
||||
@@ -401,10 +357,6 @@ import {
|
||||
TransactionExplorerConditionOperator
|
||||
} from '@/core/explorer.ts';
|
||||
|
||||
import {
|
||||
type TransactionTag
|
||||
} from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
type TransactionExplorerCondition,
|
||||
TransactionExplorerQuery
|
||||
@@ -427,8 +379,7 @@ import {
|
||||
mdiContentCopy,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiDrag,
|
||||
mdiPound
|
||||
mdiDrag
|
||||
} from '@mdi/js';
|
||||
|
||||
interface ExplorerQueryTabProps {
|
||||
@@ -460,7 +411,6 @@ const showExpression = ref<Record<string, boolean>>({});
|
||||
const showFilterSourceAccountsDialog = ref<boolean>(false);
|
||||
const showFilterDestinationAccountsDialog = ref<boolean>(false);
|
||||
const showFilterTransactionCategoriesDialog = ref<boolean>(false);
|
||||
const tagSearchContent = ref<string>('');
|
||||
const editingQuery = ref<TransactionExplorerQuery | undefined>(undefined);
|
||||
const editingQueryName = ref<string>('');
|
||||
|
||||
@@ -474,27 +424,9 @@ const queries = computed<TransactionExplorerQuery[]>({
|
||||
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||
const hasAnyAccount = computed<boolean>(() => accountsStore.allPlainAccounts.length > 0);
|
||||
const hasAnyTransactionCategory = computed<boolean>(() => !isObjectEmpty(transactionCategoriesStore.allTransactionCategoriesMap));
|
||||
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
|
||||
const allTransactionExplorerConditionFields = computed<NameValue[]>(() => getAllTransactionExplorerConditionFields());
|
||||
|
||||
const isAllFilteredTagHidden = computed<boolean>(() => {
|
||||
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
|
||||
let hiddenCount = 0;
|
||||
|
||||
for (const tag of allTags.value) {
|
||||
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
|
||||
if (!tag.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hiddenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return hiddenCount > 0;
|
||||
});
|
||||
|
||||
function getFilteredAccountsDisplayContent(filterAccountIds?: Record<string, boolean>): string {
|
||||
if ((props.loading && !hasAnyAccount.value) || !accountsStore.allVisiblePlainAccounts || !accountsStore.allVisiblePlainAccounts.length) {
|
||||
return '';
|
||||
|
||||
+481
-239
@@ -2,288 +2,406 @@
|
||||
<v-row class="match-height">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<span>{{ tt('Transaction Tags') }}</span>
|
||||
<v-btn class="ms-3" color="default" variant="outlined"
|
||||
:disabled="loading || updating || hasEditingTag" @click="add">{{ tt('Add') }}</v-btn>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating || hasEditingTag" @click="saveSortResult"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating || hasEditingTag"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading || updating || hasEditingTag" :icon="true">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
v-if="!showHidden" @click="showHidden = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiEyeOffOutline"
|
||||
:title="tt('Hide Hidden Transaction Tags')"
|
||||
v-if="showHidden" @click="showHidden = false"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-table class="transaction-tags-table table-striped" :hover="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ tt('Tag Title') }}</span>
|
||||
<v-spacer/>
|
||||
<span>{{ tt('Operation') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody v-if="loading && noAvailableTag && !newTag">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && noAvailableTag && !newTag">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
:class="{ 'has-bottom-border': newTag }"
|
||||
:disabled="noAvailableTag"
|
||||
v-model="tags"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="transaction-tags-table-row-tag text-sm" v-if="showHidden || !element.hidden">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="editingTag.id !== element.id">
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
<span class="transaction-tag-name">{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-text-field class="w-100 me-2" type="text"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating"
|
||||
:placeholder="tt('Tag Title')"
|
||||
v-model="editingTag.name"
|
||||
v-else-if="editingTag.id === element.id"
|
||||
@keyup.enter="save(editingTag)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2 ms-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="element.hidden ? mdiEyeOutline : mdiEyeOffOutline"
|
||||
:loading="tagHiding[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="hide(element, !element.hidden)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ element.hidden ? tt('Show') : tt('Hide') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiPencilOutline"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="edit(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiDeleteOutline"
|
||||
:loading="tagRemoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="remove(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Delete') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating || !isTagModified(element)"
|
||||
v-if="editingTag.id === element.id" @click="save(editingTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id === element.id" @click="cancelSave(editingTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && !hasEditingTag && availableTagCount > 1 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && !hasEditingTag && availableTagCount > 1">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
|
||||
<tbody v-if="newTag">
|
||||
<tr class="text-sm" :class="{ 'even-row': (availableTagCount & 1) === 1}">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field class="w-100 me-2" type="text" color="primary"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating" :placeholder="tt('Tag Title')"
|
||||
v-model="newTag.name" @keyup.enter="save(newTag)">
|
||||
<template #prepend>
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2" density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating['']"
|
||||
:disabled="loading || updating || !isTagModified(newTag)"
|
||||
@click="save(newTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
@click="cancelSave(newTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon class="disabled" :icon="mdiDrag"/>
|
||||
<v-layout>
|
||||
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
|
||||
<div class="mx-6 my-4">
|
||||
<span class="text-subtitle-2">{{ tt('Total tags') }}</span>
|
||||
<p class="transaction-tags-statistic-item-value mt-1">
|
||||
<span v-if="!loading || totalAvailableTagsCount > 0">{{ displayTotalAvailableTagsCount }}</span>
|
||||
<span v-else-if="loading && totalAvailableTagsCount <= 0">
|
||||
<v-skeleton-loader class="skeleton-no-margin pt-2 pb-1" type="text" :loading="true"></v-skeleton-loader>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</p>
|
||||
</div>
|
||||
<v-divider />
|
||||
<v-tabs show-arrows
|
||||
class="scrollable-vertical-tabs"
|
||||
style="max-height: calc(100% - 88px)"
|
||||
direction="vertical"
|
||||
:prev-icon="mdiMenuUp" :next-icon="mdiMenuDown"
|
||||
:disabled="loading || updating" v-model="activeTagGroupId">
|
||||
<v-tab class="tab-text-truncate" :disabled="loading || updating || hasEditingTag"
|
||||
:key="tagGroup.id" :value="tagGroup.id"
|
||||
v-for="tagGroup in allTagGroupsWithDefault"
|
||||
@click="switchTagGroup(tagGroup.id)">
|
||||
<span class="text-truncate">{{ tagGroup.name }}</span>
|
||||
</v-tab>
|
||||
<template v-if="loading && (!allTagGroupsWithDefault || allTagGroupsWithDefault.length < 1)">
|
||||
<v-skeleton-loader class="skeleton-no-margin mx-5 mt-4 mb-3" type="text"
|
||||
:key="itemIdx" :loading="true" v-for="itemIdx in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]"></v-skeleton-loader>
|
||||
</template>
|
||||
</v-tabs>
|
||||
</v-navigation-drawer>
|
||||
<v-main>
|
||||
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
|
||||
<v-window-item value="tagListPage">
|
||||
<v-card variant="flat" min-height="780">
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
|
||||
:ripple="false" :icon="true" @click="showNav = !showNav">
|
||||
<v-icon :icon="mdiMenu" size="24" />
|
||||
</v-btn>
|
||||
<span>{{ tt('Transaction Tags') }}</span>
|
||||
<v-btn class="ms-3" color="default" variant="outlined"
|
||||
:disabled="loading || updating || hasEditingTag" @click="add">{{ tt('Add') }}</v-btn>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating || hasEditingTag" @click="saveSortResult"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating || hasEditingTag"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading || updating || hasEditingTag" :icon="true">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiPlus" @click="addTagGroup">
|
||||
<v-list-item-title>{{ tt('Add Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item :prepend-icon="mdiPencilOutline"
|
||||
@click="renameTagGroup"
|
||||
v-if="activeTagGroupId && activeTagGroupId !== DEFAULT_TAG_GROUP_ID">
|
||||
<v-list-item-title>{{ tt('Rename Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item :prepend-icon="mdiDeleteOutline"
|
||||
@click="removeTagGroup"
|
||||
v-if="activeTagGroupId && activeTagGroupId !== DEFAULT_TAG_GROUP_ID">
|
||||
<v-list-item-title>{{ tt('Delete Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiSort"
|
||||
:disabled="!allTagGroupsWithDefault || allTagGroupsWithDefault.length < 2"
|
||||
:title="tt('Change Group Display Order')"
|
||||
@click="showChangeGroupDisplayOrderDialog"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
v-if="!showHidden" @click="showHidden = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiEyeOffOutline"
|
||||
:title="tt('Hide Hidden Transaction Tags')"
|
||||
v-if="showHidden" @click="showHidden = false"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-table class="transaction-tags-table table-striped" :hover="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ tt('Tag Title') }}</span>
|
||||
<v-spacer/>
|
||||
<span>{{ tt('Operation') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody v-if="loading && noAvailableTag && !newTag">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && noAvailableTag && !newTag">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
:class="{ 'has-bottom-border': newTag }"
|
||||
:disabled="noAvailableTag"
|
||||
v-model="tags"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="transaction-tags-table-row-tag text-sm" v-if="showHidden || !element.hidden">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="editingTag.id !== element.id">
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
<span class="transaction-tag-name">{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-text-field class="w-100 me-2" type="text"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating"
|
||||
:placeholder="tt('Tag Title')"
|
||||
v-model="editingTag.name"
|
||||
v-else-if="editingTag.id === element.id"
|
||||
@keyup.enter="save(editingTag)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2 ms-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="element.hidden ? mdiEyeOutline : mdiEyeOffOutline"
|
||||
:loading="tagHiding[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="hide(element, !element.hidden)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ element.hidden ? tt('Show') : tt('Hide') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiFolderMoveOutline"
|
||||
:loading="tagMoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Move') }}
|
||||
<v-menu activator="parent" max-height="500">
|
||||
<v-list>
|
||||
<v-list-subheader :title="tt('Move to...')"/>
|
||||
<template :key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault">
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="tagGroup.id" v-if="activeTagGroupId !== tagGroup.id">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="moveTagToGroup(element, tagGroup.id)">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-sm ms-3">{{ tagGroup.name }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiPencilOutline"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="edit(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiDeleteOutline"
|
||||
:loading="tagRemoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="remove(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Delete') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating || !isTagModified(element)"
|
||||
v-if="editingTag.id === element.id" @click="save(editingTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id === element.id" @click="cancelSave(editingTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && !hasEditingTag && availableTagCount > 1 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && !hasEditingTag && availableTagCount > 1">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
|
||||
<tbody v-if="newTag">
|
||||
<tr class="text-sm" :class="{ 'even-row': (availableTagCount & 1) === 1}">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field class="w-100 me-2" type="text" color="primary"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating" :placeholder="tt('Tag Title')"
|
||||
v-model="newTag.name" @keyup.enter="save(newTag)">
|
||||
<template #prepend>
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2" density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating['']"
|
||||
:disabled="loading || updating || !isTagModified(newTag)"
|
||||
@click="save(newTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
@click="cancelSave(newTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon class="disabled" :icon="mdiDrag"/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<tag-group-change-display-order-dialog ref="tagGroupChangeDisplayOrderDialog" />
|
||||
|
||||
<rename-dialog ref="renameDialog"
|
||||
:default-title="tt('Rename Tag Group')"
|
||||
:label="tt('Tag Group Name')" :placeholder="tt('Tag Group Name')" />
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TagGroupChangeDisplayOrderDialog from './dialog/TagGroupChangeDisplayOrderDialog.vue';
|
||||
import RenameDialog from '@/components/desktop/RenameDialog.vue';
|
||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
import { ref, computed, useTemplateRef, watch } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useTagListPageBase } from '@/views/base/tags/TagListPageBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
isNoAvailableTag,
|
||||
getAvailableTagCount
|
||||
} from '@/lib/tag.ts';
|
||||
import { getAvailableTagCount } from '@/lib/tag.ts';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiMenuUp,
|
||||
mdiMenuDown,
|
||||
mdiPencilOutline,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiSort,
|
||||
mdiMenu,
|
||||
mdiPlus,
|
||||
mdiFolderMoveOutline,
|
||||
mdiDeleteOutline,
|
||||
mdiDrag,
|
||||
mdiDotsVertical,
|
||||
mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
type TagGroupChangeDisplayOrderDialogType = InstanceType<typeof TagGroupChangeDisplayOrderDialog>;
|
||||
type RenameDialogType = InstanceType<typeof RenameDialog>;
|
||||
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const { tt } = useI18n();
|
||||
const display = useDisplay();
|
||||
|
||||
const { tt, formatNumberToLocalizedNumerals } = useI18n();
|
||||
|
||||
const {
|
||||
activeTagGroupId,
|
||||
newTag,
|
||||
editingTag,
|
||||
loading,
|
||||
showHidden,
|
||||
displayOrderModified,
|
||||
allTagGroupsWithDefault,
|
||||
tags,
|
||||
noAvailableTag,
|
||||
hasEditingTag,
|
||||
isTagModified,
|
||||
switchTagGroup,
|
||||
add,
|
||||
edit
|
||||
} = useTagListPageBase();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const tagGroupChangeDisplayOrderDialog = useTemplateRef<TagGroupChangeDisplayOrderDialogType>('tagGroupChangeDisplayOrderDialog');
|
||||
const renameDialog = useTemplateRef<RenameDialogType>('renameDialog');
|
||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const newTag = ref<TransactionTag | null>(null);
|
||||
const editingTag = ref<TransactionTag>(TransactionTag.createNewTag());
|
||||
const loading = ref<boolean>(true);
|
||||
const updating = ref<boolean>(false);
|
||||
const activeTab = ref<string>('tagListPage');
|
||||
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
|
||||
const showNav = ref<boolean>(display.mdAndUp.value);
|
||||
const tagUpdating = ref<Record<string, boolean>>({});
|
||||
const tagHiding = ref<Record<string, boolean>>({});
|
||||
const tagMoving = ref<Record<string, boolean>>({});
|
||||
const tagRemoving = ref<Record<string, boolean>>({});
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
const showHidden = ref<boolean>(false);
|
||||
|
||||
const tags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
const noAvailableTag = computed<boolean>(() => isNoAvailableTag(tags.value, showHidden.value));
|
||||
const totalAvailableTagsCount = computed<number>(() => transactionTagsStore.allAvailableTagsCount);
|
||||
const displayTotalAvailableTagsCount = computed<string>(() => formatNumberToLocalizedNumerals(transactionTagsStore.allAvailableTagsCount));
|
||||
const availableTagCount = computed<number>(() => getAvailableTagCount(tags.value, showHidden.value));
|
||||
const hasEditingTag = computed<boolean>(() => !!(newTag.value || (editingTag.value.id && editingTag.value.id !== '')));
|
||||
|
||||
function isTagModified(tag: TransactionTag): boolean {
|
||||
if (tag.id) {
|
||||
return editingTag.value.name !== '' && editingTag.value.name !== tag.name;
|
||||
} else {
|
||||
return tag.name !== '';
|
||||
}
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
if (hasEditingTag.value) {
|
||||
@@ -312,13 +430,125 @@ function reload(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
newTag.value = TransactionTag.createNewTag();
|
||||
function addTagGroup(): void {
|
||||
renameDialog.value?.open('', tt('New Tag Group Name')).then((newName: string) => {
|
||||
updating.value = true;
|
||||
|
||||
transactionTagsStore.saveTagGroup({
|
||||
tagGroup: TransactionTagGroup.createNewTagGroup(newName)
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function edit(tag: TransactionTag): void {
|
||||
editingTag.value.id = tag.id;
|
||||
editingTag.value.name = tag.name;
|
||||
function renameTagGroup(): void {
|
||||
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
|
||||
|
||||
if (!tagGroup) {
|
||||
snackbar.value?.showMessage('Unable to rename this tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
renameDialog.value?.open(tagGroup.name || '').then((newName: string) => {
|
||||
updating.value = true;
|
||||
|
||||
const newTagGroup = tagGroup.clone();
|
||||
newTagGroup.name = newName;
|
||||
|
||||
transactionTagsStore.saveTagGroup({
|
||||
tagGroup: newTagGroup
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showChangeGroupDisplayOrderDialog(): void {
|
||||
tagGroupChangeDisplayOrderDialog.value?.open().then(() => {
|
||||
if (transactionTagsStore.transactionTagGroupListStateInvalid) {
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagGroup(): void {
|
||||
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
|
||||
|
||||
if (!tagGroup) {
|
||||
snackbar.value?.showMessage('Unable to delete this tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTagGroupIndex = allTagGroupsWithDefault.value.findIndex(group => group.id === tagGroup.id);
|
||||
|
||||
confirmDialog.value?.open('Are you sure you want to delete this tag group?').then(() => {
|
||||
updating.value = true;
|
||||
|
||||
transactionTagsStore.deleteTagGroup({
|
||||
tagGroup: tagGroup
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
|
||||
if (transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex]) {
|
||||
const newActiveTagGroup = transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex];
|
||||
activeTagGroupId.value = newActiveTagGroup ? newActiveTagGroup.id : '';
|
||||
} else if (transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex - 1]) {
|
||||
const newActiveTagGroup = transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex - 1];
|
||||
activeTagGroupId.value = newActiveTagGroup ? newActiveTagGroup.id : '';
|
||||
} else {
|
||||
activeTagGroupId.value = '';
|
||||
}
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function moveTagToGroup(tag: TransactionTag, targetTagGroupId: string): void {
|
||||
updating.value = true;
|
||||
tagMoving.value[tag.id] = true;
|
||||
|
||||
const newTag = tag.clone();
|
||||
newTag.groupId = targetTagGroupId;
|
||||
|
||||
transactionTagsStore.saveTag({
|
||||
tag: newTag
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
tagMoving.value[tag.id] = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
tagMoving.value[tag.id] = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save(tag: TransactionTag): void {
|
||||
@@ -363,7 +593,7 @@ function saveSortResult(): void {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.updateTagDisplayOrders().then(() => {
|
||||
transactionTagsStore.updateTagDisplayOrders(activeTagGroupId.value).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
@@ -450,9 +680,21 @@ transactionTagsStore.loadAllTags({
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => display.mdAndUp.value, (newValue) => {
|
||||
alwaysShowNav.value = newValue;
|
||||
|
||||
if (!showNav.value) {
|
||||
showNav.value = newValue;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transaction-tags-statistic-item-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.transaction-tags-table tr.transaction-tags-table-row-tag .hover-display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-dialog width="800" :persistent="displayOrderModified" v-model="showState">
|
||||
<v-card class="pa-sm-1 pa-md-2">
|
||||
<template #title>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<div class="d-flex align-center">
|
||||
<h4 class="text-h4">{{ tt('Transaction Tag Groups') }}</h4>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating" @click="saveDisplayOrder"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-spacer/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card-text class="d-flex flex-column flex-md-row flex-grow-1 overflow-y-auto">
|
||||
<v-table hover density="comfortable" class="w-100 table-striped">
|
||||
<tbody v-if="loading && (!allTagGroups || allTagGroups.length < 1)">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3, 4, 5, 6 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && (!allTagGroups || allTagGroups.length < 1)">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag group') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
v-model="allTagGroups"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="text-sm">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && allTagGroups && allTagGroups.length > 0 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && allTagGroups && allTagGroups.length > 0">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="overflow-y-visible">
|
||||
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
|
||||
<v-btn color="secondary" variant="tonal"
|
||||
:disabled="loading || updating" @click="close">{{ tt('Close') }}</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { type TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiDrag
|
||||
} from '@mdi/js';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
let resolveFunc: (() => void) | null = null;
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const showState = ref<boolean>(false);
|
||||
const loading = ref<boolean>(true);
|
||||
const updating = ref<boolean>(false);
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
|
||||
const allTagGroups = computed<TransactionTagGroup[]>(() => transactionTagsStore.allTransactionTagGroups);
|
||||
|
||||
function open(): Promise<void> {
|
||||
showState.value = true;
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveFunc = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: true
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
|
||||
snackbar.value?.showMessage('Tag group list has been updated');
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (error && error.isUpToDate) {
|
||||
displayOrderModified.value = false;
|
||||
}
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveDisplayOrder(): void {
|
||||
if (!displayOrderModified.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.updateTagGroupDisplayOrders().then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
if (loading.value || updating.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolveFunc?.();
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
function onMove(event: { moved: { element: { id: string }; oldIndex: number; newIndex: number } }): void {
|
||||
if (!event || !event.moved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moveEvent = event.moved;
|
||||
|
||||
if (!moveEvent.element || !moveEvent.element.id) {
|
||||
snackbar.value?.showMessage('Unable to move tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
transactionTagsStore.changeTagGroupDisplayOrder({
|
||||
tagGroupId: moveEvent.element.id,
|
||||
from: moveEvent.oldIndex,
|
||||
to: moveEvent.newIndex
|
||||
}).then(() => {
|
||||
displayOrderModified.value = true;
|
||||
}).catch(error => {
|
||||
snackbar.value?.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
</script>
|
||||
@@ -259,15 +259,17 @@
|
||||
|
||||
<template :key="categoryType"
|
||||
v-for="(categories, categoryType) in allPrimaryCategories">
|
||||
<v-divider />
|
||||
|
||||
<v-list-item density="compact" v-show="categories && categories.length">
|
||||
<v-list-item-title>
|
||||
<span class="text-sm">{{ getTransactionTypeName(categoryTypeToTransactionType(parseInt(categoryType)), 'Type') }}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-group :key="category.id" v-for="category in categories">
|
||||
<v-list-group :key="category.id" v-for="(category, index) in categories">
|
||||
<template #activator="{ props }" v-if="!category.hidden || queryAllFilterCategoryIds[category.id] || allCategories[query.categoryIds]?.parentId === category.id || hasSubCategoryInQuery(category)">
|
||||
<v-divider />
|
||||
<v-divider v-if="index > 0" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:class="getCategoryListItemCheckedClass(category, queryAllFilterCategoryIds)"
|
||||
v-bind="props">
|
||||
@@ -474,24 +476,33 @@
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="query.tagFilter && query.tagFilter !== TransactionTagFilter.TransactionNoTagFilterValue" />
|
||||
<template :key="transactionTagGroup.id"
|
||||
v-for="transactionTagGroup in allTransactionTagGroupsWithDefault">
|
||||
<v-divider v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)" />
|
||||
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allTransactionTags">
|
||||
<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': 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(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>
|
||||
</div>
|
||||
<v-list-item density="compact" v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)">
|
||||
<v-list-item-title>
|
||||
<span class="text-sm">{{ transactionTagGroup.name }}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<template :key="transactionTag.id"
|
||||
v-for="(transactionTag, index) in (allTransactionTagsByGroup[transactionTagGroup.id] ?? [])">
|
||||
<v-divider v-if="index > 0 && (!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id]))" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="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(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>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -785,6 +796,8 @@ const {
|
||||
allCategories,
|
||||
allPrimaryCategories,
|
||||
allAvailableCategoriesCount,
|
||||
allTransactionTagGroupsWithDefault,
|
||||
allTransactionTagsByGroup,
|
||||
allTransactionTags,
|
||||
allAvailableTagsCount,
|
||||
query,
|
||||
@@ -806,6 +819,7 @@ const {
|
||||
transactionCalendarMaxDate,
|
||||
currentMonthTransactionData,
|
||||
hasSubCategoryInQuery,
|
||||
hasVisibleTagsInTagGroup,
|
||||
isSameAsDefaultTimezoneOffsetMinutes,
|
||||
canAddTransaction,
|
||||
getDisplayTime,
|
||||
|
||||
@@ -78,6 +78,7 @@ import { type NameValue, values } from '@/core/base.ts';
|
||||
import { CategoryType } from '@/core/category.ts';
|
||||
import { AUTOMATICALLY_CREATED_CATEGORY_ICON_ID } from '@/consts/icon.ts';
|
||||
import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { type TransactionCategoryCreateRequest, type TransactionCategoryCreateWithSubCategories, TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import { type TransactionTagCreateRequest, TransactionTag } from '@/models/transaction_tag.ts';
|
||||
@@ -284,12 +285,13 @@ function confirm(): void {
|
||||
const submitTags: TransactionTagCreateRequest[] = [];
|
||||
|
||||
for (const item of selectedNames.value) {
|
||||
const tag: TransactionTag = TransactionTag.createNewTag(item);
|
||||
const tag: TransactionTag = TransactionTag.createNewTag(item, DEFAULT_TAG_GROUP_ID);
|
||||
submitTags.push(tag.toCreateRequest());
|
||||
}
|
||||
|
||||
transactionTagsStore.addTags({
|
||||
tags: submitTags,
|
||||
groupId: DEFAULT_TAG_GROUP_ID,
|
||||
skipExists: true
|
||||
}).then(response => {
|
||||
transactionTagsStore.loadAllTags({ force: false }).then(() => {
|
||||
|
||||
@@ -327,57 +327,14 @@
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-autocomplete
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
auto-select-first
|
||||
persistent-placeholder
|
||||
multiple
|
||||
chips
|
||||
:closable-chips="mode !== TransactionEditPageMode.View"
|
||||
<transaction-tag-auto-complete
|
||||
:readonly="mode === TransactionEditPageMode.View"
|
||||
:disabled="loading || submitting"
|
||||
:label="tt('Tags')"
|
||||
:placeholder="tt('None')"
|
||||
:items="allTags"
|
||||
:show-label="true"
|
||||
:allow-add-new-tag="true"
|
||||
v-model="transaction.tagIds"
|
||||
v-model:search="tagSearchContent"
|
||||
>
|
||||
<template #chip="{ props, item }">
|
||||
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
|
||||
</template>
|
||||
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item :disabled="true" v-bind="props"
|
||||
v-if="item.raw.hidden && item.raw.name.toLowerCase().indexOf(tagSearchContent.toLowerCase()) >= 0 && isAllFilteredTagHidden">
|
||||
<template #title>
|
||||
<v-list-item-title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<v-list class="py-0">
|
||||
<v-list-item v-if="tagSearchContent" @click="saveNewTag(tagSearchContent)">{{ tt('format.misc.addNewTag', { tag: tagSearchContent }) }}</v-list-item>
|
||||
<v-list-item v-else-if="!tagSearchContent">{{ tt('No available tag') }}</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
@tag:saving="onSavingTag"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-textarea
|
||||
@@ -535,7 +492,6 @@ import { TemplateType, ScheduledTemplateFrequencyType } from '@/core/template.ts
|
||||
import { KnownErrorCode } from '@/consts/api.ts';
|
||||
import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
|
||||
import { Transaction } from '@/models/transaction.ts';
|
||||
@@ -567,7 +523,6 @@ import {
|
||||
mdiSwapHorizontal,
|
||||
mdiMapMarkerOutline,
|
||||
mdiCheck,
|
||||
mdiPound,
|
||||
mdiMenuDown,
|
||||
mdiImagePlusOutline,
|
||||
mdiTrashCanOutline,
|
||||
@@ -621,7 +576,6 @@ const {
|
||||
allVisibleCategorizedAccounts,
|
||||
allCategories,
|
||||
allCategoriesMap,
|
||||
allTags,
|
||||
allTagsMap,
|
||||
firstVisibleAccountId,
|
||||
hasVisibleExpenseCategories,
|
||||
@@ -669,7 +623,6 @@ const activeTab = ref<string>('basicInfo');
|
||||
const originalTransactionEditable = ref<boolean>(false);
|
||||
const noTransactionDraft = ref<boolean>(false);
|
||||
const geoMenuState = ref<boolean>(false);
|
||||
const tagSearchContent = ref<string>('');
|
||||
const removingPictureId = ref<string>('');
|
||||
|
||||
const initAmount = ref<number | undefined>(undefined);
|
||||
@@ -692,22 +645,7 @@ const sourceAmountColor = computed<string | undefined>(() => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const isAllFilteredTagHidden = computed<boolean>(() => {
|
||||
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
|
||||
let hiddenCount = 0;
|
||||
|
||||
for (const tag of allTags.value) {
|
||||
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
|
||||
if (!tag.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hiddenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return hiddenCount > 0;
|
||||
});
|
||||
|
||||
const isTransactionModified = computed<boolean>(() => {
|
||||
if (mode.value === TransactionEditPageMode.Add) {
|
||||
@@ -1141,26 +1079,6 @@ function clearGeoLocation(): void {
|
||||
transaction.value.removeGeoLocation();
|
||||
}
|
||||
|
||||
function saveNewTag(tagName: string): void {
|
||||
submitting.value = true;
|
||||
|
||||
transactionTagsStore.saveTag({
|
||||
tag: TransactionTag.createNewTag(tagName)
|
||||
}).then(tag => {
|
||||
submitting.value = false;
|
||||
|
||||
if (tag && tag.id) {
|
||||
transaction.value.tagIds.push(tag.id);
|
||||
}
|
||||
}).catch(error => {
|
||||
submitting.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showOpenPictureDialog(): void {
|
||||
if (!canAddTransactionPicture.value || submitting.value) {
|
||||
return;
|
||||
@@ -1231,6 +1149,10 @@ function viewOrRemovePicture(pictureInfo: TransactionPictureInfoBasicResponse):
|
||||
});
|
||||
}
|
||||
|
||||
function onSavingTag(state: boolean): void {
|
||||
submitting.value = state;
|
||||
}
|
||||
|
||||
function onShowDateTimeError(error: string): void {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
|
||||
@@ -49,53 +49,46 @@
|
||||
<f7-list-item :title="tt('No available tag')"></f7-list-item>
|
||||
</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="includeTagsCount > 1">
|
||||
<f7-list-item radio
|
||||
:title="tt(filterType.name)"
|
||||
:key="filterType.type"
|
||||
:value="filterType.type"
|
||||
:checked="includeTagFilterType === filterType.type"
|
||||
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
|
||||
@change="includeTagFilterType = filterType.type">
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-list class="margin-top-half margin-bottom" strong inset dividers v-if="excludeTagsCount > 1">
|
||||
<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">
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-accordion-item :opened="collapseStates['default']!.opened"
|
||||
@accordion:open="collapseStates['default']!.opened = true"
|
||||
@accordion:close="collapseStates['default']!.opened = false">
|
||||
<f7-block class="combination-list-wrapper margin-vertical"
|
||||
:key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault"
|
||||
v-show="!loading && hasAnyVisibleTag">
|
||||
<f7-accordion-item :opened="collapseStates[tagGroup.id]?.opened ?? true"
|
||||
@accordion:open="collapseStates[tagGroup.id]!.opened = true"
|
||||
@accordion:close="collapseStates[tagGroup.id]!.opened = false"
|
||||
v-if="allVisibleTags[tagGroup.id] && allVisibleTags[tagGroup.id]!.length > 0">
|
||||
<f7-block-title>
|
||||
<f7-accordion-toggle>
|
||||
<f7-list strong inset dividers
|
||||
class="combination-list-header"
|
||||
:class="collapseStates['default']!.opened ? 'combination-list-opened' : 'combination-list-closed'">
|
||||
:class="collapseStates[tagGroup.id]?.opened ? 'combination-list-opened' : 'combination-list-closed'">
|
||||
<f7-list-item group-title>
|
||||
<small>{{ tt('Tags') }}</small>
|
||||
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates['default']!.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
|
||||
<small class="tag-group-title">{{ tagGroup.name }}</small>
|
||||
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[tagGroup.id]?.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-accordion-toggle>
|
||||
</f7-block-title>
|
||||
<f7-accordion-content :style="{ height: collapseStates['default']!.opened ? 'auto' : '' }">
|
||||
<f7-accordion-content :style="{ height: collapseStates[tagGroup.id]?.opened ? 'auto' : '' }">
|
||||
<f7-list strong inset dividers accordion-list class="combination-list-content">
|
||||
<f7-list-item link="#"
|
||||
popover-open=".tag-filter-include-type-popover-menu"
|
||||
:title="tt(TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.includeType as number)?.name as string)"
|
||||
@click="currentTransactionTagGroupId = tagGroup.id"
|
||||
v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] > 1 && TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.includeType as number)">
|
||||
</f7-list-item>
|
||||
<f7-list-item link="#"
|
||||
popover-open=".tag-filter-exclude-type-popover-menu"
|
||||
:title="tt(TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.excludeType as number)?.name as string)"
|
||||
@click="currentTransactionTagGroupId = tagGroup.id"
|
||||
v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] > 1 && TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.excludeType as number)">
|
||||
</f7-list-item>
|
||||
<f7-list-item link="#"
|
||||
popover-open=".tag-filter-state-popover-menu"
|
||||
:title="transactionTag.name"
|
||||
:value="transactionTag.id"
|
||||
:key="transactionTag.id"
|
||||
:after="tt(filterTagIds[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : filterTagIds[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
|
||||
v-for="transactionTag in allVisibleTags"
|
||||
:after="tt(tagFilterStateMap[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : tagFilterStateMap[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
|
||||
v-for="transactionTag in allVisibleTags[tagGroup.id]"
|
||||
v-show="showHidden || !transactionTag.hidden"
|
||||
@click="currentTransactionTagId = transactionTag.id">
|
||||
<template #media>
|
||||
@@ -111,11 +104,41 @@
|
||||
</f7-accordion-item>
|
||||
</f7-block>
|
||||
|
||||
<f7-popover class="tag-filter-include-type-popover-menu">
|
||||
<f7-list dividers>
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="tt(filterType.name)"
|
||||
:class="{ 'list-item-selected': groupTagFilterTypesMap[currentTransactionTagGroupId]?.includeType === filterType.type }"
|
||||
:key="filterType.type"
|
||||
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
|
||||
@click="updateTransactionTagGroupIncludeType(filterType)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="groupTagFilterTypesMap[currentTransactionTagGroupId]?.includeType === filterType.type"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
<f7-popover class="tag-filter-exclude-type-popover-menu">
|
||||
<f7-list dividers>
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="tt(filterType.name)"
|
||||
:class="{ 'list-item-selected': groupTagFilterTypesMap[currentTransactionTagGroupId]?.excludeType === filterType.type }"
|
||||
:key="filterType.type"
|
||||
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
|
||||
@click="updateTransactionTagGroupExcludeType(filterType)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="groupTagFilterTypesMap[currentTransactionTagGroupId]?.excludeType === filterType.type"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
<f7-popover class="tag-filter-state-popover-menu">
|
||||
<f7-list dividers>
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="state.displayName"
|
||||
:class="{ 'list-item-selected': filterTagIds[currentTransactionTagId] === state.type }"
|
||||
:class="{ 'list-item-selected': tagFilterStateMap[currentTransactionTagId] === state.type }"
|
||||
:key="state.type"
|
||||
v-for="state in [
|
||||
{ type: TransactionTagFilterState.Include, displayName: tt('Included') },
|
||||
@@ -124,7 +147,7 @@
|
||||
]"
|
||||
@click="updateCurrentTransactionTagState(state.type)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="filterTagIds[currentTransactionTagId] === state.type"></f7-icon>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="tagFilterStateMap[currentTransactionTagId] === state.type"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
@@ -160,6 +183,7 @@ import {
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { values } from '@/core/base.ts';
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
|
||||
interface CollapseState {
|
||||
@@ -180,13 +204,13 @@ const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterContent,
|
||||
filterTagIds,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
tagFilterStateMap,
|
||||
groupTagFilterTypesMap,
|
||||
title,
|
||||
groupTagFilterStateCountMap,
|
||||
allTagGroupsWithDefault,
|
||||
allVisibleTags,
|
||||
allVisibleTagGroupIds,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
loadFilterTagIds,
|
||||
@@ -196,20 +220,30 @@ const {
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const currentTransactionTagGroupId = ref<string>('');
|
||||
const currentTransactionTagId = ref<string>('');
|
||||
const showMoreActionSheet = ref<boolean>(false);
|
||||
|
||||
const collapseStates = ref<Record<string, CollapseState>>({
|
||||
default: {
|
||||
opened: true
|
||||
const collapseStates = ref<Record<string, CollapseState>>(getInitCollapseState(allVisibleTagGroupIds.value));
|
||||
|
||||
function getInitCollapseState(tagGroupIds: string[]): Record<string, CollapseState> {
|
||||
const states: Record<string, CollapseState> = {};
|
||||
|
||||
for (const tagGroupId of tagGroupIds) {
|
||||
states[tagGroupId] = {
|
||||
opened: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
transactionTagsStore.loadAllTags({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
collapseStates.value = getInitCollapseState(allVisibleTagGroupIds.value);
|
||||
|
||||
if (!loadFilterTagIds()) {
|
||||
showToast('Parameter Invalid');
|
||||
@@ -225,14 +259,36 @@ function init(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function updateTransactionTagGroupIncludeType(filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[currentTransactionTagGroupId.value];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.includeType = filterType.type;
|
||||
}
|
||||
|
||||
function updateTransactionTagGroupExcludeType(filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[currentTransactionTagGroupId.value];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.excludeType = filterType.type;
|
||||
}
|
||||
|
||||
function updateCurrentTransactionTagState(state: number): void {
|
||||
filterTagIds.value[currentTransactionTagId.value] = state;
|
||||
tagFilterStateMap.value[currentTransactionTagId.value] = state;
|
||||
currentTransactionTagId.value = '';
|
||||
}
|
||||
|
||||
function setAllTagsState(value: TransactionTagFilterState): void {
|
||||
for (const tag of allVisibleTags.value) {
|
||||
filterTagIds.value[tag.id] = value;
|
||||
for (const tags of values(allVisibleTags.value)) {
|
||||
for (const tag of tags) {
|
||||
tagFilterStateMap.value[tag.id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,3 +307,10 @@ function onPageAfterIn(): void {
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tag-group-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
<f7-nav-left v-else-if="sortable">
|
||||
<f7-link icon-f7="xmark" :class="{ 'disabled': displayOrderSaving }" @click="cancelSort"></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title :title="tt('Transaction Tags')"></f7-nav-title>
|
||||
<f7-nav-title>
|
||||
<f7-link popover-open=".tag-group-popover-menu" :class="{ 'disabled': sortable || hasEditingTag }">
|
||||
<span style="color: var(--f7-text-color)">{{ displayTagGroupName }}</span>
|
||||
<f7-icon class="page-title-bar-icon" color="gray" style="opacity: 0.5" f7="chevron_down_circle_fill"></f7-icon>
|
||||
</f7-link>
|
||||
</f7-nav-title>
|
||||
<f7-nav-right class="navbar-compact-icons">
|
||||
<f7-link icon-f7="ellipsis" :class="{ 'disabled': hasEditingTag || !tags.length || sortable }" @click="showMoreActionSheet = true"></f7-link>
|
||||
<f7-link icon-f7="plus" :class="{ 'disabled': hasEditingTag }" v-if="!sortable" @click="add"></f7-link>
|
||||
@@ -13,6 +18,22 @@
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<f7-popover class="tag-group-popover-menu"
|
||||
@popover:open="scrollPopoverToSelectedItem">
|
||||
<f7-list dividers>
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="tagGroup.name"
|
||||
:class="{ 'list-item-selected': activeTagGroupId === tagGroup.id }"
|
||||
:key="tagGroup.id"
|
||||
v-for="tagGroup in allTagGroupsWithDefault"
|
||||
@click="switchTagGroup(tagGroup.id)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="activeTagGroupId === tagGroup.id"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
<f7-list strong inset dividers class="tag-item-list margin-top skeleton-text" v-if="loading">
|
||||
<f7-list-item :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||
<template #media>
|
||||
@@ -155,18 +176,17 @@ import { ref, computed } from 'vue';
|
||||
import type { Router } from 'framework7/types';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useI18nUIComponents, showLoading, hideLoading, onSwipeoutDeleted } from '@/lib/ui/mobile.ts';
|
||||
import { type Framework7Dom, useI18nUIComponents, showLoading, hideLoading, onSwipeoutDeleted } from '@/lib/ui/mobile.ts';
|
||||
import { useTagListPageBase } from '@/views/base/tags/TagListPageBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { TextDirection } from '@/core/text.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
isNoAvailableTag,
|
||||
getFirstShowingId,
|
||||
getLastShowingId
|
||||
} from '@/lib/tag.ts';
|
||||
import { scrollToSelectedItem } from '@/lib/ui/common.ts';
|
||||
import { getFirstShowingId, getLastShowingId } from '@/lib/tag.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
f7router: Router.Router;
|
||||
@@ -175,34 +195,40 @@ const props = defineProps<{
|
||||
const { tt, getCurrentLanguageTextDirection } = useI18n();
|
||||
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
|
||||
|
||||
const {
|
||||
activeTagGroupId,
|
||||
newTag,
|
||||
editingTag,
|
||||
loading,
|
||||
showHidden,
|
||||
displayOrderModified,
|
||||
allTagGroupsWithDefault,
|
||||
tags,
|
||||
noAvailableTag,
|
||||
hasEditingTag,
|
||||
isTagModified,
|
||||
switchTagGroup,
|
||||
add,
|
||||
edit
|
||||
} = useTagListPageBase();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const newTag = ref<TransactionTag | null>(null);
|
||||
const editingTag = ref<TransactionTag>(TransactionTag.createNewTag());
|
||||
const loading = ref<boolean>(true);
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const showHidden = ref<boolean>(false);
|
||||
const sortable = ref<boolean>(false);
|
||||
const tagToDelete = ref<TransactionTag | null>(null);
|
||||
const showMoreActionSheet = ref<boolean>(false);
|
||||
const showDeleteActionSheet = ref<boolean>(false);
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
const displayOrderSaving = ref<boolean>(false);
|
||||
|
||||
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
|
||||
const tags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
const firstShowingId = computed<string | null>(() => getFirstShowingId(tags.value, showHidden.value));
|
||||
const lastShowingId = computed<string | null>(() => getLastShowingId(tags.value, showHidden.value));
|
||||
const noAvailableTag = computed<boolean>(() => isNoAvailableTag(tags.value, showHidden.value));
|
||||
const hasEditingTag = computed<boolean>(() => !!(newTag.value || (editingTag.value.id && editingTag.value.id !== '')));
|
||||
|
||||
function isTagModified(tag: TransactionTag): boolean {
|
||||
if (tag.id) {
|
||||
return editingTag.value.name !== '' && editingTag.value.name !== tag.name;
|
||||
} else {
|
||||
return tag.name !== '';
|
||||
}
|
||||
}
|
||||
const displayTagGroupName = computed<string>(() => {
|
||||
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
|
||||
return tagGroup ? tagGroup.name : tt('Default Group');
|
||||
});
|
||||
|
||||
function getTagDomId(tag: TransactionTag): string {
|
||||
return 'tag_' + tag.id;
|
||||
@@ -258,15 +284,6 @@ function reload(done?: () => void): void {
|
||||
});
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
newTag.value = TransactionTag.createNewTag();
|
||||
}
|
||||
|
||||
function edit(tag: TransactionTag): void {
|
||||
editingTag.value.id = tag.id;
|
||||
editingTag.value.name = tag.name;
|
||||
}
|
||||
|
||||
function save(tag: TransactionTag): void {
|
||||
showLoading();
|
||||
|
||||
@@ -368,7 +385,7 @@ function saveSortResult(): void {
|
||||
displayOrderSaving.value = true;
|
||||
showLoading();
|
||||
|
||||
transactionTagsStore.updateTagDisplayOrders().then(() => {
|
||||
transactionTagsStore.updateTagDisplayOrders(activeTagGroupId.value).then(() => {
|
||||
displayOrderSaving.value = false;
|
||||
hideLoading();
|
||||
|
||||
@@ -438,6 +455,10 @@ function onSort(event: { el: { id: string }, from: number, to: number }): void {
|
||||
});
|
||||
}
|
||||
|
||||
function scrollPopoverToSelectedItem(event: { $el: Framework7Dom }): void {
|
||||
scrollToSelectedItem(event.$el[0], '.popover-inner', '.popover-inner', 'li.list-item-selected');
|
||||
}
|
||||
|
||||
function onPageAfterIn(): void {
|
||||
if (transactionTagsStore.transactionTagListStateInvalid && !loading.value) {
|
||||
reload();
|
||||
@@ -458,4 +479,9 @@ init();
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-group-popover-menu .popover-inner {
|
||||
max-height: 440px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -367,7 +367,7 @@
|
||||
<template #footer>
|
||||
<f7-block class="margin-top-half no-padding no-margin" v-if="transaction.tagIds && transaction.tagIds.length">
|
||||
<f7-chip media-text-color="var(--f7-chip-text-color)" class="transaction-edit-tag"
|
||||
:text="getTagName(tagId)"
|
||||
:text="allTagsMap[tagId]?.name ?? ''"
|
||||
:key="tagId"
|
||||
v-for="tagId in transaction.tagIds">
|
||||
<template #media>
|
||||
@@ -570,7 +570,6 @@ const {
|
||||
allVisibleCategorizedAccounts,
|
||||
allCategories,
|
||||
allCategoriesMap,
|
||||
allTags,
|
||||
allTagsMap,
|
||||
firstVisibleAccountId,
|
||||
hasVisibleExpenseCategories,
|
||||
@@ -828,16 +827,6 @@ function getFontClassByAmount(amount: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getTagName(tagId: string): string {
|
||||
for (const tag of allTags.value) {
|
||||
if (tag.id === tagId) {
|
||||
return tag.name;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
if (!pageTypeAndMode) {
|
||||
showToast('Parameter Invalid');
|
||||
|
||||
@@ -558,24 +558,31 @@
|
||||
</template>
|
||||
</f7-list-item>
|
||||
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="transactionTag.name"
|
||||
: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 || 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="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
|
||||
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
</f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
<template :key="transactionTagGroup.id"
|
||||
v-for="transactionTagGroup in allTransactionTagGroupsWithDefault">
|
||||
<f7-list-item group-title class="transaction-tag-group" v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)">
|
||||
<small>{{ transactionTagGroup.name }}</small>
|
||||
</f7-list-item>
|
||||
|
||||
<f7-list-item link="#" no-chevron popover-close
|
||||
:title="transactionTag.name"
|
||||
: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 (allTransactionTagsByGroup[transactionTagGroup.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="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
|
||||
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
</f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</template>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
@@ -686,6 +693,8 @@ const {
|
||||
allCategories,
|
||||
allPrimaryCategories,
|
||||
allAvailableCategoriesCount,
|
||||
allTransactionTagGroupsWithDefault,
|
||||
allTransactionTagsByGroup,
|
||||
allTransactionTags,
|
||||
allAvailableTagsCount,
|
||||
displayPageTypeName,
|
||||
@@ -708,6 +717,7 @@ const {
|
||||
transactionCalendarMaxDate,
|
||||
currentMonthTransactionData,
|
||||
hasSubCategoryInQuery,
|
||||
hasVisibleTagsInTagGroup,
|
||||
isSameAsDefaultTimezoneOffsetMinutes,
|
||||
canAddTransaction,
|
||||
getDisplayTime,
|
||||
@@ -1608,6 +1618,15 @@ html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-foo
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.more-popover-menu .transaction-tag-group {
|
||||
background-color: inherit;
|
||||
|
||||
> small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-calendar-container .dp__theme_light,
|
||||
.transaction-calendar-container .dp__theme_dark {
|
||||
--dp-background-color: var(--f7-list-strong-bg-color);
|
||||
|
||||
Reference in New Issue
Block a user