add transaction tag filter to frontend

This commit is contained in:
MaysWind
2024-07-21 23:36:16 +08:00
parent 4f9ab9db75
commit 26d77de67a
15 changed files with 874 additions and 14 deletions
@@ -0,0 +1,284 @@
<template>
<v-card :class="{ 'pa-2 pa-sm-4 pa-md-8': dialogMode }">
<template #title>
<div class="d-flex align-center justify-center" v-if="dialogMode">
<div class="w-100 text-center">
<h4 class="text-h4">{{ $t(title) }}</h4>
</div>
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
:disabled="loading || !hasAnyAvailableTag" :icon="true">
<v-icon :icon="icons.more" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="icons.selectAll"
:title="$t('Select All')"
@click="selectAll"></v-list-item>
<v-list-item :prepend-icon="icons.selectNone"
:title="$t('Select None')"
@click="selectNone"></v-list-item>
<v-list-item :prepend-icon="icons.selectInverse"
:title="$t('Invert Selection')"
@click="selectInvert"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
<div class="d-flex align-center" v-else-if="!dialogMode">
<span>{{ $t(title) }}</span>
<v-spacer/>
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
:disabled="loading" :icon="true">
<v-icon :icon="icons.more" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="icons.selectAll"
:title="$t('Select All')"
@click="selectAll"></v-list-item>
<v-list-item :prepend-icon="icons.selectNone"
:title="$t('Select None')"
@click="selectNone"></v-list-item>
<v-list-item :prepend-icon="icons.selectInverse"
:title="$t('Invert Selection')"
@click="selectInvert"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
<div v-if="loading">
<v-skeleton-loader type="paragraph" :loading="loading"
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]"></v-skeleton-loader>
</div>
<v-card-text :class="{ 'mt-0 mt-sm-2 mt-md-4': dialogMode }" v-if="!loading && !hasAnyAvailableTag">
<span class="text-body-1">{{ $t('No available tag') }}</span>
</v-card-text>
<v-card-text :class="{ 'mt-0 mt-sm-2 mt-md-4': dialogMode }" v-else-if="!loading && hasAnyAvailableTag">
<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="ml-3">{{ $t('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-checkbox :model-value="!filterTagIds[transactionTag.id]"
@update:model-value="selectTransactionTag(transactionTag, $event)">
<template #label>
<v-icon size="24" :icon="icons.tag"/>
<span class="ml-3">{{ transactionTag.name }}</span>
</template>
</v-checkbox>
</template>
</v-list-item>
</template>
</v-list>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<v-card-text class="overflow-y-visible" v-if="dialogMode">
<div class="w-100 d-flex justify-center mt-2 mt-sm-4 mt-md-6 gap-4">
<v-btn :disabled="!hasAnyAvailableTag" @click="save">{{ $t(applyText) }}</v-btn>
<v-btn color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn>
</div>
</v-card-text>
</v-card>
</template>
<script>
import { mapStores } from 'pinia';
import { useTransactionTagsStore } from '@/stores/transactionTag.js';
import { useTransactionsStore } from '@/stores/transaction.js';
import {
selectAll,
selectNone,
selectInvert
} from '@/lib/common.js';
import {
mdiSelectAll,
mdiSelect,
mdiSelectInverse,
mdiDotsVertical,
mdiPound
} from '@mdi/js';
export default {
props: [
'dialogMode',
'type',
'autoSave'
],
emits: [
'settings:change'
],
data: function () {
return {
loading: true,
expandTagCategories: [ 'default' ],
filterTagIds: {},
icons: {
selectAll: mdiSelectAll,
selectNone: mdiSelect,
selectInverse: mdiSelectInverse,
more: mdiDotsVertical,
tag: mdiPound
}
}
},
computed: {
...mapStores(useTransactionTagsStore, useTransactionsStore),
title() {
return 'Filter Transaction Tags';
},
applyText() {
return 'Apply';
},
allVisibleTags() {
return this.transactionTagsStore.allVisibleTags;
},
hasAnyAvailableTag() {
return this.transactionTagsStore.allVisibleTagsCount > 0;
}
},
created() {
const self = this;
self.transactionTagsStore.loadAllTags({
force: false
}).then(() => {
self.loading = false;
const allTransactionTagIds = {};
for (let transactionTagId in self.transactionTagsStore.allTransactionTagsMap) {
if (!Object.prototype.hasOwnProperty.call(self.transactionTagsStore.allTransactionTagsMap, transactionTagId)) {
continue;
}
const transactionTag = self.transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (self.type === 'transactionListCurrent' && self.transactionsStore.allFilterTagIdsCount > 0) {
allTransactionTagIds[transactionTag.id] = true;
} else {
allTransactionTagIds[transactionTag.id] = false;
}
}
if (self.type === 'transactionListCurrent') {
for (let transactionTagId in self.transactionsStore.allFilterTagIds) {
if (!Object.prototype.hasOwnProperty.call(self.transactionsStore.allFilterTagIds, transactionTagId)) {
continue;
}
const transactionTag = self.transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (transactionTag) {
allTransactionTagIds[transactionTag.id] = false;
}
}
self.filterTagIds = allTransactionTagIds;
} else {
self.$refs.snackbar.showError('Parameter Invalid');
}
}).catch(error => {
self.loading = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
methods: {
save() {
const self = this;
const filteredTagIds = {};
let isAllSelected = true;
let finalTagIds = '';
let changed = true;
for (let transactionTagId in self.filterTagIds) {
if (!Object.prototype.hasOwnProperty.call(self.filterTagIds, transactionTagId)) {
continue;
}
const transactionTag = self.transactionTagsStore.allTransactionTagsMap[transactionTagId];
if (self.filterTagIds[transactionTag.id]) {
filteredTagIds[transactionTag.id] = true;
isAllSelected = false;
} else {
if (finalTagIds.length > 0) {
finalTagIds += ',';
}
finalTagIds += transactionTag.id;
}
}
if (this.type === 'transactionListCurrent') {
changed = self.transactionsStore.updateTransactionListFilter({
tagIds: isAllSelected ? '' : finalTagIds
});
if (changed) {
self.transactionsStore.updateTransactionListInvalidState(true);
}
}
self.$emit('settings:change', changed);
},
cancel() {
this.$emit('settings:change', false);
},
selectTransactionTag(transactionTag, value) {
this.filterTagIds[transactionTag.id] = !value;
if (this.autoSave) {
this.save();
}
},
selectAll() {
selectAll(this.filterTagIds, this.transactionTagsStore.allTransactionTagsMap);
if (this.autoSave) {
this.save();
}
},
selectNone() {
selectNone(this.filterTagIds, this.transactionTagsStore.allTransactionTagsMap);
if (this.autoSave) {
this.save();
}
},
selectInvert() {
selectInvert(this.filterTagIds, this.transactionTagsStore.allTransactionTagsMap);
if (this.autoSave) {
this.save();
}
}
}
}
</script>
<style>
.tag-categories .v-expansion-panel-text__wrapper {
padding: 0 0 0 20px;
}
.tag-categories .v-expansion-panel--active:not(:first-child),
.tag-categories .v-expansion-panel--active + .v-expansion-panel {
margin-top: 1rem;
}
</style>