mirror of
https://github.com/mayswind/ezbookkeeping
synced 2026-04-21 13:37:43 +00:00
support transaction tag group
This commit is contained in:
parent
b556efa510
commit
7d9cfc4ced
59 changed files with 3289 additions and 795 deletions
|
|
@ -101,6 +101,14 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
|||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagGroup))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag group table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -418,6 +418,14 @@ func startWebServer(c *core.CliContext) error {
|
|||
apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler))
|
||||
apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler))
|
||||
|
||||
// Transaction Tag Groups
|
||||
apiV1Route.GET("/transaction/tags/groups/list.json", bindApi(api.TransactionTagGroups.TagGroupListHandler))
|
||||
apiV1Route.GET("/transaction/tags/groups/get.json", bindApi(api.TransactionTagGroups.TagGroupGetHandler))
|
||||
apiV1Route.POST("/transaction/tags/groups/add.json", bindApi(api.TransactionTagGroups.TagGroupCreateHandler))
|
||||
apiV1Route.POST("/transaction/tags/groups/modify.json", bindApi(api.TransactionTagGroups.TagGroupModifyHandler))
|
||||
apiV1Route.POST("/transaction/tags/groups/move.json", bindApi(api.TransactionTagGroups.TagGroupMoveHandler))
|
||||
apiV1Route.POST("/transaction/tags/groups/delete.json", bindApi(api.TransactionTagGroups.TagGroupDeleteHandler))
|
||||
|
||||
// Transaction Tags
|
||||
apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler))
|
||||
apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler))
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type DataManagementsApi struct {
|
|||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
tagGroups *services.TransactionTagGroupService
|
||||
pictures *services.TransactionPictureService
|
||||
templates *services.TransactionTemplateService
|
||||
userCustomExchangeRates *services.UserCustomExchangeRatesService
|
||||
|
|
@ -46,6 +47,7 @@ var (
|
|||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
tagGroups: services.TransactionTagGroups,
|
||||
pictures: services.TransactionPictures,
|
||||
templates: services.TransactionTemplates,
|
||||
userCustomExchangeRates: services.UserCustomExchangeRates,
|
||||
|
|
@ -193,6 +195,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
|
|||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.tagGroups.DeleteAllTagGroups(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction tag groups, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
210
pkg/api/transaction_tag_groups.go
Normal file
210
pkg/api/transaction_tag_groups.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
)
|
||||
|
||||
// TransactionTagGroupsApi represents transaction tag group api
|
||||
type TransactionTagGroupsApi struct {
|
||||
tagGroups *services.TransactionTagGroupService
|
||||
}
|
||||
|
||||
// Initialize a transaction tag group api singleton instance
|
||||
var (
|
||||
TransactionTagGroups = &TransactionTagGroupsApi{
|
||||
tagGroups: services.TransactionTagGroups,
|
||||
}
|
||||
)
|
||||
|
||||
// TagGroupListHandler returns transaction tag group list of current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
tagGroups, err := a.tagGroups.GetAllTagGroupsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupListHandler] failed to get tag groups for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
tagGroupResps := make(models.TransactionTagGroupInfoResponseSlice, len(tagGroups))
|
||||
|
||||
for i := 0; i < len(tagGroups); i++ {
|
||||
tagGroupResps[i] = tagGroups[i].ToTransactionTagGroupInfoResponse()
|
||||
}
|
||||
|
||||
sort.Sort(tagGroupResps)
|
||||
|
||||
return tagGroupResps, nil
|
||||
}
|
||||
|
||||
// TagGroupGetHandler returns one specific transaction tag group of current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var tagGroupGetReq models.TransactionTagGroupGetRequest
|
||||
err := c.ShouldBindQuery(&tagGroupGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tag_groups.TagGroupGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagGroupGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupGetHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
|
||||
|
||||
return tagGroupResp, nil
|
||||
}
|
||||
|
||||
// TagGroupCreateHandler saves a new transaction tag group by request parameters for current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var tagGroupCreateReq models.TransactionTagGroupCreateRequest
|
||||
err := c.ShouldBindJSON(&tagGroupCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tag_groups.TagGroupCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
maxOrderId, err := a.tagGroups.GetMaxDisplayOrder(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
tagGroup := a.createNewTagGroupModel(uid, &tagGroupCreateReq, maxOrderId+1)
|
||||
|
||||
err = a.tagGroups.CreateTagGroup(c, tagGroup)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupCreateHandler] failed to create tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroup.TagGroupId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tag_groups.TagGroupCreateHandler] user \"uid:%d\" has created a new tag group \"id:%d\" successfully", uid, tagGroup.TagGroupId)
|
||||
|
||||
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
|
||||
|
||||
return tagGroupResp, nil
|
||||
}
|
||||
|
||||
// TagGroupModifyHandler saves an existed transaction tag group by request parameters for current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var tagGroupModifyReq models.TransactionTagGroupModifyRequest
|
||||
err := c.ShouldBindJSON(&tagGroupModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tag_groups.TagGroupModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagGroupModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupModifyHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
newTagGroup := &models.TransactionTagGroup{
|
||||
TagGroupId: tagGroup.TagGroupId,
|
||||
Uid: uid,
|
||||
Name: tagGroupModifyReq.Name,
|
||||
}
|
||||
|
||||
if newTagGroup.Name == tagGroup.Name {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
err = a.tagGroups.ModifyTagGroup(c, newTagGroup)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupModifyHandler] failed to update tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tag_groups.TagGroupModifyHandler] user \"uid:%d\" has updated tag group \"id:%d\" successfully", uid, tagGroupModifyReq.Id)
|
||||
|
||||
tagGroup.Name = newTagGroup.Name
|
||||
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
|
||||
|
||||
return tagGroupResp, nil
|
||||
}
|
||||
|
||||
// TagGroupMoveHandler moves display order of existed transaction tag groups by request parameters for current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var tagGroupMoveReq models.TransactionTagGroupMoveRequest
|
||||
err := c.ShouldBindJSON(&tagGroupMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tag_groups.TagGroupMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
tagGroups := make([]*models.TransactionTagGroup, len(tagGroupMoveReq.NewDisplayOrders))
|
||||
|
||||
for i := 0; i < len(tagGroupMoveReq.NewDisplayOrders); i++ {
|
||||
newDisplayOrder := tagGroupMoveReq.NewDisplayOrders[i]
|
||||
tagGroup := &models.TransactionTagGroup{
|
||||
Uid: uid,
|
||||
TagGroupId: newDisplayOrder.Id,
|
||||
DisplayOrder: newDisplayOrder.DisplayOrder,
|
||||
}
|
||||
|
||||
tagGroups[i] = tagGroup
|
||||
}
|
||||
|
||||
err = a.tagGroups.ModifyTagGroupDisplayOrders(c, uid, tagGroups)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupMoveHandler] failed to move tag groups for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tag_groups.TagGroupMoveHandler] user \"uid:%d\" has moved tag groups", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TagGroupDeleteHandler deletes an existed transaction tag group by request parameters for current user
|
||||
func (a *TransactionTagGroupsApi) TagGroupDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var tagGroupDeleteReq models.TransactionTagGroupDeleteRequest
|
||||
err := c.ShouldBindJSON(&tagGroupDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tag_groups.TagGroupDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.tagGroups.DeleteTagGroup(c, uid, tagGroupDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tag_groups.TagGroupDeleteHandler] failed to delete tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tag_groups.TagGroupDeleteHandler] user \"uid:%d\" has deleted tag group \"id:%d\"", uid, tagGroupDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *TransactionTagGroupsApi) createNewTagGroupModel(uid int64, tagGroupCreateReq *models.TransactionTagGroupCreateRequest, order int32) *models.TransactionTagGroup {
|
||||
return &models.TransactionTagGroup{
|
||||
Uid: uid,
|
||||
Name: tagGroupCreateReq.Name,
|
||||
DisplayOrder: order,
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
|
|||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
|
|
@ -111,9 +111,16 @@ func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *er
|
|||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
|
||||
if tagCreateBatchReq.Tags[i].GroupId != tagCreateBatchReq.GroupId {
|
||||
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the group id \"%d\" of tag#%d is inconsistent with the batch group id \"%d\"", tagCreateBatchReq.Tags[i].GroupId, i, tagCreateBatchReq.GroupId)
|
||||
return nil, errs.ErrTransactionTagGroupIdInvalid
|
||||
}
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
|
|
@ -161,16 +168,31 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
|||
}
|
||||
|
||||
newTag := &models.TransactionTag{
|
||||
TagId: tag.TagId,
|
||||
Uid: uid,
|
||||
Name: tagModifyReq.Name,
|
||||
TagId: tag.TagId,
|
||||
Uid: uid,
|
||||
Name: tagModifyReq.Name,
|
||||
TagGroupId: tagModifyReq.GroupId,
|
||||
DisplayOrder: tag.DisplayOrder,
|
||||
}
|
||||
|
||||
if newTag.Name == tag.Name {
|
||||
tagNameChanged := newTag.Name != tag.Name
|
||||
|
||||
if !tagNameChanged && newTag.TagGroupId == tag.TagGroupId {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
err = a.tags.ModifyTag(c, newTag)
|
||||
if newTag.TagGroupId != tag.TagGroupId {
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, newTag.TagGroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
newTag.DisplayOrder = maxOrderId + 1
|
||||
}
|
||||
|
||||
err = a.tags.ModifyTag(c, newTag, tagNameChanged)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
||||
|
|
@ -180,6 +202,8 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
|||
log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
|
||||
|
||||
tag.Name = newTag.Name
|
||||
tag.TagGroupId = newTag.TagGroupId
|
||||
tag.DisplayOrder = newTag.DisplayOrder
|
||||
tagResp := tag.ToTransactionTagInfoResponse()
|
||||
|
||||
return tagResp, nil
|
||||
|
|
@ -268,6 +292,7 @@ func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.T
|
|||
return &models.TransactionTag{
|
||||
Uid: uid,
|
||||
Name: tagCreateReq.Name,
|
||||
TagGroupId: tagCreateReq.GroupId,
|
||||
DisplayOrder: order,
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +303,7 @@ func (a *TransactionTagsApi) createNewTagModels(uid int64, tagCreateBatchReq *mo
|
|||
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
|
||||
tagCreateReq := tagCreateBatchReq.Tags[i]
|
||||
tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i))
|
||||
tag.TagGroupId = tagCreateBatchReq.GroupId
|
||||
tags[i] = tag
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const (
|
|||
NormalSubcategoryUserExternalAuth = 16
|
||||
NormalSubcategoryOAuth2 = 17
|
||||
NormalSubcategoryInsightsExplorer = 18
|
||||
NormalSubcategoryTagGroup = 19
|
||||
)
|
||||
|
||||
// Error represents the specific error returned to user
|
||||
|
|
|
|||
10
pkg/errs/transaction_tag_group.go
Normal file
10
pkg/errs/transaction_tag_group.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package errs
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Error codes related to transaction tag groups
|
||||
var (
|
||||
ErrTransactionTagGroupIdInvalid = NewNormalError(NormalSubcategoryTagGroup, 0, http.StatusBadRequest, "transaction tag group id is invalid")
|
||||
ErrTransactionTagGroupNotFound = NewNormalError(NormalSubcategoryTagGroup, 1, http.StatusBadRequest, "transaction tag group not found")
|
||||
ErrTransactionTagGroupInUseCannotBeDeleted = NewNormalError(NormalSubcategoryTagGroup, 2, http.StatusBadRequest, "transaction tag group is in use and cannot be deleted")
|
||||
)
|
||||
|
|
@ -3,10 +3,11 @@ package models
|
|||
// TransactionTag represents transaction tag data stored in database
|
||||
type TransactionTag struct {
|
||||
TagId int64 `xorm:"PK"`
|
||||
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_group_order) NOT NULL"`
|
||||
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_group_order) NOT NULL"`
|
||||
TagGroupId int64 `xorm:"INDEX(IDX_tag_uid_deleted_group_order) NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_group_order) NOT NULL"`
|
||||
Hidden bool `xorm:"NOT NULL"`
|
||||
CreatedUnixTime int64
|
||||
UpdatedUnixTime int64
|
||||
|
|
@ -20,19 +21,22 @@ type TransactionTagGetRequest struct {
|
|||
|
||||
// TransactionTagCreateRequest represents all parameters of transaction tag creation request
|
||||
type TransactionTagCreateRequest struct {
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
GroupId int64 `json:"groupId,string"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
}
|
||||
|
||||
// TransactionTagCreateBatchRequest represents all parameters of transaction tag batch creation request
|
||||
type TransactionTagCreateBatchRequest struct {
|
||||
Tags []*TransactionTagCreateRequest `json:"tags" binding:"required"`
|
||||
GroupId int64 `json:"groupId,string"`
|
||||
SkipExists bool `json:"skipExists"`
|
||||
}
|
||||
|
||||
// TransactionTagModifyRequest represents all parameters of transaction tag modification request
|
||||
type TransactionTagModifyRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
GroupId int64 `json:"groupId,string"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
}
|
||||
|
||||
// TransactionTagHideRequest represents all parameters of transaction tag hiding request
|
||||
|
|
@ -61,6 +65,7 @@ type TransactionTagDeleteRequest struct {
|
|||
type TransactionTagInfoResponse struct {
|
||||
Id int64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
TagGroupId int64 `json:"groupId,string"`
|
||||
DisplayOrder int32 `json:"displayOrder"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
|
@ -71,6 +76,7 @@ func (t *TransactionTag) FillFromOtherTag(tag *TransactionTag) {
|
|||
t.Uid = tag.Uid
|
||||
t.Deleted = tag.Deleted
|
||||
t.Name = tag.Name
|
||||
t.TagGroupId = tag.TagGroupId
|
||||
t.DisplayOrder = tag.DisplayOrder
|
||||
t.Hidden = tag.Hidden
|
||||
t.CreatedUnixTime = tag.CreatedUnixTime
|
||||
|
|
@ -83,6 +89,7 @@ func (t *TransactionTag) ToTransactionTagInfoResponse() *TransactionTagInfoRespo
|
|||
return &TransactionTagInfoResponse{
|
||||
Id: t.TagId,
|
||||
Name: t.Name,
|
||||
TagGroupId: t.TagGroupId,
|
||||
DisplayOrder: t.DisplayOrder,
|
||||
Hidden: t.Hidden,
|
||||
}
|
||||
|
|
@ -103,5 +110,9 @@ func (s TransactionTagInfoResponseSlice) Swap(i, j int) {
|
|||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (s TransactionTagInfoResponseSlice) Less(i, j int) bool {
|
||||
if s[i].TagGroupId != s[j].TagGroupId {
|
||||
return s[i].TagGroupId < s[j].TagGroupId
|
||||
}
|
||||
|
||||
return s[i].DisplayOrder < s[j].DisplayOrder
|
||||
}
|
||||
|
|
|
|||
79
pkg/models/transaction_tag_group.go
Normal file
79
pkg/models/transaction_tag_group.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package models
|
||||
|
||||
// TransactionTagGroup represents transaction tag group data stored in database
|
||||
type TransactionTagGroup struct {
|
||||
TagGroupId int64 `xorm:"PK"`
|
||||
Uid int64 `xorm:"INDEX(IDX_tag_group_uid_deleted_order) NOT NULL"`
|
||||
Deleted bool `xorm:"INDEX(IDX_tag_group_uid_deleted_order) NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
DisplayOrder int32 `xorm:"INDEX(IDX_tag_group_uid_deleted_order) NOT NULL"`
|
||||
CreatedUnixTime int64
|
||||
UpdatedUnixTime int64
|
||||
DeletedUnixTime int64
|
||||
}
|
||||
|
||||
// TransactionTagGroupGetRequest represents all parameters of transaction tag group getting request
|
||||
type TransactionTagGroupGetRequest struct {
|
||||
Id int64 `form:"id,string" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupCreateRequest represents all parameters of transaction tag group creation request
|
||||
type TransactionTagGroupCreateRequest struct {
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupModifyRequest represents all parameters of transaction tag group modification request
|
||||
type TransactionTagGroupModifyRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupMoveRequest represents all parameters of transaction tag group moving request
|
||||
type TransactionTagGroupMoveRequest struct {
|
||||
NewDisplayOrders []*TransactionTagGroupNewDisplayOrderRequest `json:"newDisplayOrders" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupNewDisplayOrderRequest represents a data pair of id and display order
|
||||
type TransactionTagGroupNewDisplayOrderRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
DisplayOrder int32 `json:"displayOrder"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupDeleteRequest represents all parameters of transaction tag group deleting request
|
||||
type TransactionTagGroupDeleteRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransactionTagGroupInfoResponse represents a view-object of transaction tag group
|
||||
type TransactionTagGroupInfoResponse struct {
|
||||
Id int64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
DisplayOrder int32 `json:"displayOrder"`
|
||||
}
|
||||
|
||||
// ToTransactionTagGroupInfoResponse returns a view-object according to database model
|
||||
func (t *TransactionTagGroup) ToTransactionTagGroupInfoResponse() *TransactionTagGroupInfoResponse {
|
||||
return &TransactionTagGroupInfoResponse{
|
||||
Id: t.TagGroupId,
|
||||
Name: t.Name,
|
||||
DisplayOrder: t.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
// TransactionTagGroupInfoResponseSlice represents the slice data structure of TransactionTagGroupInfoResponse
|
||||
type TransactionTagGroupInfoResponseSlice []*TransactionTagGroupInfoResponse
|
||||
|
||||
// Len returns the count of items
|
||||
func (s TransactionTagGroupInfoResponseSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s TransactionTagGroupInfoResponseSlice) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (s TransactionTagGroupInfoResponseSlice) Less(i, j int) bool {
|
||||
return s[i].DisplayOrder < s[j].DisplayOrder
|
||||
}
|
||||
30
pkg/models/transaction_tag_index_test.go
Normal file
30
pkg/models/transaction_tag_index_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTransactionTagGroupInfoResponseSliceLess(t *testing.T) {
|
||||
var transactionTagGroupRespSlice TransactionTagGroupInfoResponseSlice
|
||||
transactionTagGroupRespSlice = append(transactionTagGroupRespSlice, &TransactionTagGroupInfoResponse{
|
||||
Id: 1,
|
||||
DisplayOrder: 3,
|
||||
})
|
||||
transactionTagGroupRespSlice = append(transactionTagGroupRespSlice, &TransactionTagGroupInfoResponse{
|
||||
Id: 2,
|
||||
DisplayOrder: 1,
|
||||
})
|
||||
transactionTagGroupRespSlice = append(transactionTagGroupRespSlice, &TransactionTagGroupInfoResponse{
|
||||
Id: 3,
|
||||
DisplayOrder: 2,
|
||||
})
|
||||
|
||||
sort.Sort(transactionTagGroupRespSlice)
|
||||
|
||||
assert.Equal(t, int64(2), transactionTagGroupRespSlice[0].Id)
|
||||
assert.Equal(t, int64(3), transactionTagGroupRespSlice[1].Id)
|
||||
assert.Equal(t, int64(1), transactionTagGroupRespSlice[2].Id)
|
||||
}
|
||||
|
|
@ -11,20 +11,41 @@ func TestTransactionTagInfoResponseSliceLess(t *testing.T) {
|
|||
var transactionTagRespSlice TransactionTagInfoResponseSlice
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 1,
|
||||
TagGroupId: 0,
|
||||
DisplayOrder: 3,
|
||||
})
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 2,
|
||||
DisplayOrder: 1,
|
||||
TagGroupId: 1,
|
||||
DisplayOrder: 2,
|
||||
})
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 3,
|
||||
TagGroupId: 0,
|
||||
DisplayOrder: 1,
|
||||
})
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 4,
|
||||
TagGroupId: 2,
|
||||
DisplayOrder: 1,
|
||||
})
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 5,
|
||||
TagGroupId: 1,
|
||||
DisplayOrder: 1,
|
||||
})
|
||||
transactionTagRespSlice = append(transactionTagRespSlice, &TransactionTagInfoResponse{
|
||||
Id: 6,
|
||||
TagGroupId: 0,
|
||||
DisplayOrder: 2,
|
||||
})
|
||||
|
||||
sort.Sort(transactionTagRespSlice)
|
||||
|
||||
assert.Equal(t, int64(2), transactionTagRespSlice[0].Id)
|
||||
assert.Equal(t, int64(3), transactionTagRespSlice[1].Id)
|
||||
assert.Equal(t, int64(3), transactionTagRespSlice[0].Id)
|
||||
assert.Equal(t, int64(6), transactionTagRespSlice[1].Id)
|
||||
assert.Equal(t, int64(1), transactionTagRespSlice[2].Id)
|
||||
assert.Equal(t, int64(5), transactionTagRespSlice[3].Id)
|
||||
assert.Equal(t, int64(2), transactionTagRespSlice[4].Id)
|
||||
assert.Equal(t, int64(4), transactionTagRespSlice[5].Id)
|
||||
}
|
||||
|
|
|
|||
220
pkg/services/transaction_tag_groups.go
Normal file
220
pkg/services/transaction_tag_groups.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||
)
|
||||
|
||||
// TransactionTagGroupService represents transaction tag group service
|
||||
type TransactionTagGroupService struct {
|
||||
ServiceUsingDB
|
||||
ServiceUsingUuid
|
||||
}
|
||||
|
||||
// Initialize a transaction tag group service singleton instance
|
||||
var (
|
||||
TransactionTagGroups = &TransactionTagGroupService{
|
||||
ServiceUsingDB: ServiceUsingDB{
|
||||
container: datastore.Container,
|
||||
},
|
||||
ServiceUsingUuid: ServiceUsingUuid{
|
||||
container: uuid.Container,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// GetAllTagGroupsByUid returns all transaction tag group models of user
|
||||
func (s *TransactionTagGroupService) GetAllTagGroupsByUid(c core.Context, uid int64) ([]*models.TransactionTagGroup, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
var tagGroups []*models.TransactionTagGroup
|
||||
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Find(&tagGroups)
|
||||
|
||||
return tagGroups, err
|
||||
}
|
||||
|
||||
// GetTagGroupByTagGroupId returns a transaction tag group model according to transaction tag group id
|
||||
func (s *TransactionTagGroupService) GetTagGroupByTagGroupId(c core.Context, uid int64, tagGroupId int64) (*models.TransactionTagGroup, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
if tagGroupId <= 0 {
|
||||
return nil, errs.ErrTransactionTagGroupIdInvalid
|
||||
}
|
||||
|
||||
tagGroup := &models.TransactionTagGroup{}
|
||||
has, err := s.UserDataDB(uid).NewSession(c).ID(tagGroupId).Where("uid=? AND deleted=?", uid, false).Get(tagGroup)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
|
||||
return tagGroup, nil
|
||||
}
|
||||
|
||||
// GetMaxDisplayOrder returns the max display order
|
||||
func (s *TransactionTagGroupService) GetMaxDisplayOrder(c core.Context, uid int64) (int32, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
tagGroup := &models.TransactionTagGroup{}
|
||||
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=?", uid, false).OrderBy("display_order desc").Limit(1).Get(tagGroup)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if has {
|
||||
return tagGroup.DisplayOrder, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTagGroup saves a new transaction tag group model to database
|
||||
func (s *TransactionTagGroupService) CreateTagGroup(c core.Context, tagGroup *models.TransactionTagGroup) error {
|
||||
if tagGroup.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
tagGroup.TagGroupId = s.GenerateUuid(uuid.UUID_TYPE_TAG_GROUP)
|
||||
|
||||
if tagGroup.TagGroupId < 1 {
|
||||
return errs.ErrSystemIsBusy
|
||||
}
|
||||
|
||||
tagGroup.Deleted = false
|
||||
tagGroup.CreatedUnixTime = time.Now().Unix()
|
||||
tagGroup.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(tagGroup.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
_, err := sess.Insert(tagGroup)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ModifyTagGroup saves an existed transaction tag group model to database
|
||||
func (s *TransactionTagGroupService) ModifyTagGroup(c core.Context, tagGroup *models.TransactionTagGroup) error {
|
||||
if tagGroup.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
tagGroup.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(tagGroup.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.ID(tagGroup.TagGroupId).Cols("name", "updated_unix_time").Where("uid=? AND deleted=?", tagGroup.Uid, false).Update(tagGroup)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ModifyTagGroupDisplayOrders updates display order of given transaction tag groups
|
||||
func (s *TransactionTagGroupService) ModifyTagGroupDisplayOrders(c core.Context, uid int64, tagGroups []*models.TransactionTagGroup) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
for i := 0; i < len(tagGroups); i++ {
|
||||
tagGroups[i].UpdatedUnixTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
for i := 0; i < len(tagGroups); i++ {
|
||||
tagGroup := tagGroups[i]
|
||||
updatedRows, err := sess.ID(tagGroup.TagGroupId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(tagGroup)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTagGroup deletes an existed transaction tag group from database
|
||||
func (s *TransactionTagGroupService) DeleteTagGroup(c core.Context, uid int64, tagGroupId int64) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.TransactionTagGroup{
|
||||
Deleted: true,
|
||||
DeletedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
exists, err := sess.Cols("uid", "deleted").Where("uid=? AND deleted=? AND tag_group_id=?", uid, false, tagGroupId).Limit(1).Exist(&models.TransactionTag{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return errs.ErrTransactionTagGroupInUseCannotBeDeleted
|
||||
}
|
||||
|
||||
deletedRows, err := sess.ID(tagGroupId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if deletedRows < 1 {
|
||||
return errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllTagGroups deletes all existed transaction tag groups from database
|
||||
func (s *TransactionTagGroupService) DeleteAllTagGroups(c core.Context, uid int64) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.TransactionTagGroup{
|
||||
Deleted: true,
|
||||
DeletedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
exists, err := sess.Cols("uid", "deleted").Where("uid=? AND deleted=? AND tag_group_id>?", uid, false, 0).Limit(1).Exist(&models.TransactionTag{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return errs.ErrTransactionTagGroupInUseCannotBeDeleted
|
||||
}
|
||||
|
||||
_, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -101,13 +101,13 @@ func (s *TransactionTagService) GetTagsByTagIds(c core.Context, uid int64, tagId
|
|||
}
|
||||
|
||||
// GetMaxDisplayOrder returns the max display order
|
||||
func (s *TransactionTagService) GetMaxDisplayOrder(c core.Context, uid int64) (int32, error) {
|
||||
func (s *TransactionTagService) GetMaxDisplayOrder(c core.Context, uid int64, tagGroupId int64) (int32, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
tag := &models.TransactionTag{}
|
||||
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=?", uid, false).OrderBy("display_order desc").Limit(1).Get(tag)
|
||||
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=? AND tag_group_id=?", uid, false, tagGroupId).OrderBy("display_order desc").Limit(1).Get(tag)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
@ -294,23 +294,25 @@ func (s *TransactionTagService) CreateTags(c core.Context, uid int64, tags []*mo
|
|||
}
|
||||
|
||||
// ModifyTag saves an existed transaction tag model to database
|
||||
func (s *TransactionTagService) ModifyTag(c core.Context, tag *models.TransactionTag) error {
|
||||
func (s *TransactionTagService) ModifyTag(c core.Context, tag *models.TransactionTag, tagNameChanged bool) error {
|
||||
if tag.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
exists, err := s.ExistsTagName(c, tag.Uid, tag.Name)
|
||||
if tagNameChanged {
|
||||
exists, err := s.ExistsTagName(c, tag.Uid, tag.Name)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return errs.ErrTransactionTagNameAlreadyExists
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return errs.ErrTransactionTagNameAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
tag.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(tag.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.ID(tag.TagId).Cols("name", "updated_unix_time").Where("uid=? AND deleted=?", tag.Uid, false).Update(tag)
|
||||
updatedRows, err := sess.ID(tag.TagId).Cols("name", "tag_group_id", "display_order", "updated_unix_time").Where("uid=? AND deleted=?", tag.Uid, false).Update(tag)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ const (
|
|||
UUID_TYPE_TEMPLATE UuidType = 7
|
||||
UUID_TYPE_PICTURE UuidType = 8
|
||||
UUID_TYPE_EXPLORER UuidType = 9
|
||||
UUID_TYPE_TAG_GROUP UuidType = 10
|
||||
)
|
||||
|
|
|
|||
141
src/components/base/TransactionTagSelectionBase.ts
Normal file
141
src/components/base/TransactionTagSelectionBase.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { ref, computed } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { values } from '@/core/base.ts';
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
export type TransactionTagWithGroupHeader = TransactionTag | {
|
||||
type: 'subheader';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface CommonTransactionTagSelectionProps {
|
||||
modelValue: string[];
|
||||
allowAddNewTag?: boolean;
|
||||
}
|
||||
|
||||
export function useTransactionTagSelectionBase(props: CommonTransactionTagSelectionProps, useClonedModelValue?: boolean) {
|
||||
const { tt } = useI18n();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const clonedModelValue = ref<string[]>(useClonedModelValue ? Array.from(props.modelValue) : []);
|
||||
const tagSearchContent = ref<string>('');
|
||||
|
||||
const selectedTagIds = computed<Record<string, boolean>>(() => {
|
||||
const ret: Record<string, boolean> = {};
|
||||
|
||||
if (useClonedModelValue) {
|
||||
for (const tagId of clonedModelValue.value) {
|
||||
ret[tagId] = true;
|
||||
}
|
||||
} else {
|
||||
for (const tagId of props.modelValue) {
|
||||
ret[tagId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
const lowerCaseTagSearchContent = computed<string>(() => tagSearchContent.value.toLowerCase());
|
||||
|
||||
const allTagsWithGroupHeader = computed<TransactionTagWithGroupHeader[]>(() => getTagsWithGroupHeader(tag => {
|
||||
if (!tag.hidden) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedTagIds.value[tag.id]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lowerCaseTagSearchContent.value && tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent.value) >= 0 && isAllFilteredTagHidden.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
|
||||
const filteredTagsWithGroupHeader = computed<TransactionTagWithGroupHeader[]>(() => getTagsWithGroupHeader(tag => {
|
||||
if (lowerCaseTagSearchContent.value) {
|
||||
if (tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent.value) >= 0 && (!tag.hidden || isAllFilteredTagHidden.value)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !tag.hidden || !!selectedTagIds.value[tag.id];
|
||||
}));
|
||||
|
||||
const isAllFilteredTagHidden = computed<boolean>(() => {
|
||||
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
|
||||
let hiddenCount = 0;
|
||||
|
||||
for (const tag of values(transactionTagsStore.allTransactionTagsMap)) {
|
||||
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
|
||||
if (!tag.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hiddenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return hiddenCount > 0;
|
||||
});
|
||||
|
||||
function getTagsWithGroupHeader(tagFilterFn: (tag: TransactionTag) => boolean): TransactionTagWithGroupHeader[] {
|
||||
const result: TransactionTagWithGroupHeader[] = [];
|
||||
const tagsInDefaultGroup = transactionTagsStore.allTransactionTagsByGroupMap[DEFAULT_TAG_GROUP_ID];
|
||||
|
||||
if (tagsInDefaultGroup && tagsInDefaultGroup.length > 0) {
|
||||
const visibleTags = tagsInDefaultGroup.filter(tag => tagFilterFn(tag));
|
||||
|
||||
if (visibleTags.length > 0) {
|
||||
result.push({
|
||||
type: 'subheader',
|
||||
title: tt('Default Group')
|
||||
});
|
||||
|
||||
result.push(...visibleTags);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tagGroup of transactionTagsStore.allTransactionTagGroups) {
|
||||
const tags = transactionTagsStore.allTransactionTagsByGroupMap[tagGroup.id];
|
||||
|
||||
if (!tags || tags.length < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const visibleTags = tags.filter(tag => tagFilterFn(tag));
|
||||
|
||||
if (visibleTags.length > 0) {
|
||||
result.push({
|
||||
type: 'subheader',
|
||||
title: tagGroup.name
|
||||
});
|
||||
|
||||
result.push(...visibleTags);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
// states
|
||||
clonedModelValue,
|
||||
tagSearchContent,
|
||||
// computed states
|
||||
selectedTagIds,
|
||||
allTagsWithGroupHeader,
|
||||
filteredTagsWithGroupHeader
|
||||
};
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<v-dialog max-width="500" :persistent="oldExplorerName !== newExplorerName" v-model="showState">
|
||||
<v-dialog max-width="500" :persistent="oldName !== newName" 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>
|
||||
<h4 class="text-h4 text-wrap">{{ dialogTitle || defaultTitle }}</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"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
v-model="newName"
|
||||
@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">
|
||||
<v-btn color="primary" :disabled="!newName || oldName === newName" @click="save">
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn color="secondary" variant="tonal" @click="cancel">
|
||||
|
|
@ -31,6 +31,12 @@ import { ref } from 'vue';
|
|||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
defineProps<{
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
defaultTitle?: string;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
let resolveFunc: ((name: string) => void) | null = null;
|
||||
|
|
@ -38,14 +44,14 @@ 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>('');
|
||||
const oldName = ref<string>('');
|
||||
const newName = ref<string>('');
|
||||
|
||||
function open(currentExplorerName: string, title?: string): Promise<string> {
|
||||
function open(currentName: string, title?: string): Promise<string> {
|
||||
showState.value = true;
|
||||
dialogTitle.value = title;
|
||||
oldExplorerName.value = currentExplorerName;
|
||||
newExplorerName.value = currentExplorerName;
|
||||
oldName.value = currentName;
|
||||
newName.value = currentName;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveFunc = resolve;
|
||||
|
|
@ -54,11 +60,11 @@ function open(currentExplorerName: string, title?: string): Promise<string> {
|
|||
}
|
||||
|
||||
function save(): void {
|
||||
if (!newExplorerName.value || oldExplorerName.value === newExplorerName.value) {
|
||||
if (!newName.value || oldName.value === newName.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolveFunc?.(newExplorerName.value);
|
||||
resolveFunc?.(newName.value);
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
134
src/components/desktop/TransactionTagAutoComplete.vue
Normal file
134
src/components/desktop/TransactionTagAutoComplete.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<v-autocomplete
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
auto-select-first
|
||||
persistent-placeholder
|
||||
multiple
|
||||
chips
|
||||
:density="density"
|
||||
:variant="variant"
|
||||
:closable-chips="!readonly"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:label="showLabel ? tt('Tags') : undefined"
|
||||
:placeholder="tt('None')"
|
||||
:items="allTagsWithGroupHeader"
|
||||
:model-value="modelValue"
|
||||
v-model:search="tagSearchContent"
|
||||
@update:modelValue="updateModelValue"
|
||||
>
|
||||
<template #chip="{ props, item }">
|
||||
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
|
||||
</template>
|
||||
|
||||
<template #subheader="{ props }">
|
||||
<v-list-subheader>{{ props['title'] }}</v-list-subheader>
|
||||
</template>
|
||||
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item :value="item.value" v-bind="props" v-if="item.raw instanceof TransactionTag && !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-else-if="item.raw instanceof TransactionTag && 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>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<v-list class="py-0">
|
||||
<v-list-item v-if="tagSearchContent && allowAddNewTag" @click="saveNewTag(tagSearchContent)">{{ tt('format.misc.addNewTag', { tag: tagSearchContent }) }}</v-list-item>
|
||||
<v-list-item v-else-if="!tagSearchContent || !allowAddNewTag">{{ tt('No available tag') }}</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { type CommonTransactionTagSelectionProps, useTransactionTagSelectionBase } from '@/components/base/TransactionTagSelectionBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import type { ComponentDensity, InputVariant } from '@/lib/ui/desktop.ts';
|
||||
|
||||
import {
|
||||
mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
interface DesktopTransactionTagSelectionProps extends CommonTransactionTagSelectionProps {
|
||||
density?: ComponentDensity;
|
||||
variant?: InputVariant;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<DesktopTransactionTagSelectionProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void;
|
||||
(e: 'tag:saving', state: boolean, tagName: string): void;
|
||||
}>();
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const {
|
||||
tagSearchContent,
|
||||
allTagsWithGroupHeader
|
||||
} = useTransactionTagSelectionBase(props);
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
function saveNewTag(tagName: string): void {
|
||||
emit('tag:saving', true, tagName);
|
||||
|
||||
transactionTagsStore.saveTag({
|
||||
tag: TransactionTag.createNewTag(tagName)
|
||||
}).then(tag => {
|
||||
emit('tag:saving', false, tagName);
|
||||
|
||||
if (tag && tag.id) {
|
||||
const newValue: string[] = [...props.modelValue];
|
||||
newValue.push(tag.id);
|
||||
updateModelValue(newValue);
|
||||
}
|
||||
}).catch(error => {
|
||||
emit('tag:saving', false, tagName);
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateModelValue(newValue: string[]) {
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -8,43 +8,55 @@
|
|||
<f7-link sheet-close icon-f7="xmark"></f7-link>
|
||||
</div>
|
||||
<f7-searchbar ref="searchbar" custom-searchs
|
||||
:value="filterContent"
|
||||
:value="tagSearchContent"
|
||||
:placeholder="tt('Find tag')"
|
||||
:disable-button="false"
|
||||
v-if="enableFilter"
|
||||
@input="filterContent = $event.target.value"
|
||||
@input="tagSearchContent = $event.target.value"
|
||||
@focus="onSearchBarFocus">
|
||||
</f7-searchbar>
|
||||
<div class="right">
|
||||
<f7-button round fill icon-f7="checkmark_alt" @click="save"
|
||||
v-if="allTags && allTags.length && !noAvailableTag"></f7-button>
|
||||
v-if="filteredTagsWithGroupHeader && filteredTagsWithGroupHeader.length > 0"></f7-button>
|
||||
<f7-link icon-f7="plus" :class="{'disabled': newTag}" @click="addNewTag"
|
||||
v-if="!allTags || !allTags.length || noAvailableTag"></f7-link>
|
||||
v-if="!filteredTagsWithGroupHeader || filteredTagsWithGroupHeader.length < 1"></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-page-content :class="'margin-top ' + heightClass">
|
||||
<f7-list class="no-margin-top no-margin-bottom" v-if="(!allTags || !allTags.length || noAvailableTag) && !newTag">
|
||||
<f7-list class="no-margin-top no-margin-bottom" v-if="(!filteredTagsWithGroupHeader || filteredTagsWithGroupHeader.length < 1) && !newTag">
|
||||
<f7-list-item :title="tt('No available tag')"></f7-list-item>
|
||||
</f7-list>
|
||||
<f7-list dividers class="no-margin-top no-margin-bottom tag-selection-list" v-else-if="(allTags && allTags.length && !noAvailableTag) || newTag">
|
||||
<f7-list-item checkbox
|
||||
:class="isChecked(tag.id) ? 'list-item-selected' : ''"
|
||||
:value="tag.id"
|
||||
:checked="isChecked(tag.id)"
|
||||
:key="tag.id"
|
||||
v-for="tag in allTags"
|
||||
@change="changeTagSelection">
|
||||
<template #title>
|
||||
<f7-block class="no-padding no-margin">
|
||||
<f7-list dividers class="no-margin-top no-margin-bottom tag-selection-list" v-else-if="(filteredTagsWithGroupHeader && filteredTagsWithGroupHeader.length > 0) || newTag">
|
||||
<template :key="(tag instanceof TransactionTag) ? tag.id : `${tag.type}-${index}-${tag.title}`"
|
||||
v-for="(tag, index) in filteredTagsWithGroupHeader">
|
||||
<f7-list-item group-title v-if="!(tag instanceof TransactionTag)">
|
||||
<div class="tag-selection-list-item">
|
||||
{{ tag.title }}
|
||||
</div>
|
||||
</f7-list-item>
|
||||
<f7-list-item checkbox
|
||||
:class="{ 'list-item-selected': selectedTagIds[tag.id], 'disabled': tag.hidden && !selectedTagIds[tag.id] }"
|
||||
:value="tag.id"
|
||||
:checked="selectedTagIds[tag.id]"
|
||||
:key="tag.id"
|
||||
v-else-if="tag instanceof TransactionTag"
|
||||
@change="changeTagSelection">
|
||||
<template #media>
|
||||
<f7-icon class="transaction-tag-icon" f7="number">
|
||||
<f7-badge color="gray" class="right-bottom-icon" v-if="tag.hidden">
|
||||
<f7-icon f7="eye_slash_fill"></f7-icon>
|
||||
</f7-badge>
|
||||
</f7-icon>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="display-flex">
|
||||
<f7-icon class="transaction-tag-icon" f7="number"></f7-icon>
|
||||
<div class="tag-selection-list-item list-item-valign-middle padding-inline-start-half">
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
</div>
|
||||
</f7-block>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</template>
|
||||
<f7-list-item link="#" no-chevron
|
||||
:title="tt('Add new tag')"
|
||||
v-if="allowAddNewTag && !newTag"
|
||||
|
|
@ -85,11 +97,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import type { Sheet, Searchbar } from 'framework7/types';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
|
||||
import { type CommonTransactionTagSelectionProps, useTransactionTagSelectionBase } from '@/components/base/TransactionTagSelectionBase.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
|
@ -97,12 +110,12 @@ import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
|||
import { scrollToSelectedItem } from '@/lib/ui/common.ts';
|
||||
import { type Framework7Dom, scrollSheetToTop } from '@/lib/ui/mobile.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string[];
|
||||
allowAddNewTag?: boolean;
|
||||
interface MobileransactionTagSelectionProps extends CommonTransactionTagSelectionProps {
|
||||
enableFilter?: boolean;
|
||||
show: boolean;
|
||||
}>();
|
||||
}
|
||||
|
||||
const props = defineProps<MobileransactionTagSelectionProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void;
|
||||
|
|
@ -112,81 +125,49 @@ const emit = defineEmits<{
|
|||
const { tt } = useI18n();
|
||||
const { showToast } = useI18nUIComponents();
|
||||
|
||||
const {
|
||||
clonedModelValue,
|
||||
tagSearchContent,
|
||||
selectedTagIds,
|
||||
filteredTagsWithGroupHeader
|
||||
} = useTransactionTagSelectionBase(props, true);
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const sheet = useTemplateRef<Sheet.Sheet>('sheet');
|
||||
const searchbar = useTemplateRef<Searchbar.Searchbar>('searchbar');
|
||||
|
||||
const filterContent = ref<string>('');
|
||||
const selectedItemIds = ref<string[]>(Array.from(props.modelValue));
|
||||
const newTag = ref<TransactionTag | null>(null);
|
||||
const heightClass = ref<string>(getHeightClass());
|
||||
|
||||
const allTags = computed<TransactionTag[]>(() => {
|
||||
const finalTags: TransactionTag[] = [];
|
||||
|
||||
for (const tag of transactionTagsStore.allTransactionTags) {
|
||||
if (tag.hidden && !isChecked(tag.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!props.enableFilter || !filterContent.value) {
|
||||
finalTags.push(tag);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag.name.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0) {
|
||||
finalTags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return finalTags;
|
||||
});
|
||||
|
||||
const noAvailableTag = computed<boolean>(() => {
|
||||
if (transactionTagsStore.allTransactionTags) {
|
||||
for (const transactionTag of transactionTagsStore.allTransactionTags) {
|
||||
if (!transactionTag.hidden) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
function getHeightClass(): string {
|
||||
if (transactionTagsStore.allTransactionTags && transactionTagsStore.allVisibleTagsCount > 6) {
|
||||
if (filteredTagsWithGroupHeader.value.length > 6) {
|
||||
return 'tag-selection-huge-sheet';
|
||||
} else if (transactionTagsStore.allTransactionTags && transactionTagsStore.allVisibleTagsCount > 3) {
|
||||
} else if (filteredTagsWithGroupHeader.value.length > 3) {
|
||||
return 'tag-selection-large-sheet';
|
||||
} else {
|
||||
return 'tag-selection-default-sheet';
|
||||
}
|
||||
}
|
||||
|
||||
function isChecked(itemId: string): boolean {
|
||||
return selectedItemIds.value.indexOf(itemId) >= 0;
|
||||
}
|
||||
|
||||
function changeTagSelection(e: Event): void {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const tagId = target.value;
|
||||
const index = selectedItemIds.value.indexOf(tagId);
|
||||
const index = clonedModelValue.value.indexOf(tagId);
|
||||
|
||||
if (target.checked) {
|
||||
if (index < 0) {
|
||||
selectedItemIds.value.push(tagId);
|
||||
clonedModelValue.value.push(tagId);
|
||||
}
|
||||
} else {
|
||||
if (index >= 0) {
|
||||
selectedItemIds.value.splice(index, 1);
|
||||
clonedModelValue.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
emit('update:modelValue', selectedItemIds.value);
|
||||
emit('update:modelValue', clonedModelValue.value);
|
||||
emit('update:show', false);
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +189,7 @@ function saveNewTag(): void {
|
|||
newTag.value = null;
|
||||
|
||||
if (tag && tag.id) {
|
||||
selectedItemIds.value.push(tag.id);
|
||||
clonedModelValue.value.push(tag.id);
|
||||
}
|
||||
}).catch(error => {
|
||||
hideLoading();
|
||||
|
|
@ -228,14 +209,14 @@ function onSearchBarFocus(): void {
|
|||
}
|
||||
|
||||
function onSheetOpen(event: { $el: Framework7Dom }): void {
|
||||
selectedItemIds.value = Array.from(props.modelValue);
|
||||
clonedModelValue.value = Array.from(props.modelValue);
|
||||
newTag.value = null;
|
||||
scrollToSelectedItem(event.$el[0], '.sheet-modal-inner', '.page-content', 'li.list-item-selected');
|
||||
}
|
||||
|
||||
function onSheetClosed(): void {
|
||||
emit('update:show', false);
|
||||
filterContent.value = '';
|
||||
tagSearchContent.value = '';
|
||||
searchbar.value?.clear();
|
||||
}
|
||||
</script>
|
||||
|
|
@ -262,6 +243,12 @@ function onSheetClosed(): void {
|
|||
}
|
||||
}
|
||||
|
||||
.tag-selection-list.list.list-dividers li.list-group-title:first-child,
|
||||
.tag-selection-list.list.list-dividers li.list-group-title.actual-first-child {
|
||||
margin-top: 10px;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.tag-selection-list.list .item-media + .item-inner {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
|
|
|||
1
src/consts/tag.ts
Normal file
1
src/consts/tag.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const DEFAULT_TAG_GROUP_ID: string = '0';
|
||||
|
|
@ -94,6 +94,7 @@ import FiscalYearStartSelect from '@/components/desktop/FiscalYearStartSelect.vu
|
|||
import ColorSelect from '@/components/desktop/ColorSelect.vue';
|
||||
import IconSelect from '@/components/desktop/IconSelect.vue';
|
||||
import TwoColumnSelect from '@/components/desktop/TwoColumnSelect.vue';
|
||||
import TransactionTagAutoComplete from '@/components/desktop/TransactionTagAutoComplete.vue';
|
||||
import ScheduleFrequencySelect from '@/components/desktop/ScheduleFrequencySelect.vue';
|
||||
import StepsBar from '@/components/desktop/StepsBar.vue';
|
||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
|
|
@ -102,6 +103,7 @@ import PieChartComponent from '@/components/desktop/PieChart.vue';
|
|||
import RadarChartComponent from '@/components/desktop/RadarChart.vue';
|
||||
import AxisChart from '@/components/desktop/AxisChart.vue';
|
||||
import TrendsChart from '@/components/desktop/TrendsChart.vue';
|
||||
import RenameDialog from '@/components/desktop/RenameDialog.vue';
|
||||
import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue';
|
||||
import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue';
|
||||
import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue';
|
||||
|
|
@ -539,6 +541,7 @@ app.component('FiscalYearStartSelect', FiscalYearStartSelect);
|
|||
app.component('ColorSelect', ColorSelect);
|
||||
app.component('IconSelect', IconSelect);
|
||||
app.component('TwoColumnSelect', TwoColumnSelect);
|
||||
app.component('TransactionTagAutoComplete', TransactionTagAutoComplete);
|
||||
app.component('ScheduleFrequencySelect', ScheduleFrequencySelect);
|
||||
app.component('StepsBar', StepsBar);
|
||||
app.component('ConfirmDialog', ConfirmDialog);
|
||||
|
|
@ -547,6 +550,7 @@ app.component('PieChart', PieChartComponent);
|
|||
app.component('RadarChart', RadarChartComponent);
|
||||
app.component('AxisChart', AxisChart);
|
||||
app.component('TrendsChart', TrendsChart);
|
||||
app.component('RenameDialog', RenameDialog);
|
||||
app.component('DateRangeSelectionDialog', DateRangeSelectionDialog);
|
||||
app.component('MonthSelectionDialog', MonthSelectionDialog);
|
||||
app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,13 @@ import type {
|
|||
TransactionPictureUnusedDeleteRequest,
|
||||
TransactionPictureInfoBasicResponse
|
||||
} from '@/models/transaction_picture_info.ts';
|
||||
import type {
|
||||
TransactionTagGroupCreateRequest,
|
||||
TransactionTagGroupModifyRequest,
|
||||
TransactionTagGroupMoveRequest,
|
||||
TransactionTagGroupDeleteRequest,
|
||||
TransactionTagGroupInfoResponse
|
||||
} from '@/models/transaction_tag_group.ts';
|
||||
import type {
|
||||
TransactionTagCreateRequest,
|
||||
TransactionTagCreateBatchRequest,
|
||||
|
|
@ -705,6 +712,24 @@ export default {
|
|||
deleteTransactionCategory: (req: TransactionCategoryDeleteRequest): ApiResponsePromise<boolean> => {
|
||||
return axios.post<ApiResponse<boolean>>('v1/transaction/categories/delete.json', req);
|
||||
},
|
||||
getAllTransactionTagGroups: (): ApiResponsePromise<TransactionTagGroupInfoResponse[]> => {
|
||||
return axios.get<ApiResponse<TransactionTagInfoResponse[]>>('v1/transaction/tags/groups/list.json');
|
||||
},
|
||||
getTransactionTagGroup: ({ id }: { id: string }): ApiResponsePromise<TransactionTagGroupInfoResponse> => {
|
||||
return axios.get<ApiResponse<TransactionTagInfoResponse>>('v1/transaction/tags/groups/get.json?id=' + id);
|
||||
},
|
||||
addTransactionTagGroup: (req: TransactionTagGroupCreateRequest): ApiResponsePromise<TransactionTagGroupInfoResponse> => {
|
||||
return axios.post<ApiResponse<TransactionTagInfoResponse>>('v1/transaction/tags/groups/add.json', req);
|
||||
},
|
||||
modifyTransactionTagGroup: (req: TransactionTagGroupModifyRequest): ApiResponsePromise<TransactionTagGroupInfoResponse> => {
|
||||
return axios.post<ApiResponse<TransactionTagInfoResponse>>('v1/transaction/tags/groups/modify.json', req);
|
||||
},
|
||||
moveTransactionTagGroup: (req: TransactionTagGroupMoveRequest): ApiResponsePromise<boolean> => {
|
||||
return axios.post<ApiResponse<boolean>>('v1/transaction/tags/groups/move.json', req);
|
||||
},
|
||||
deleteTransactionTagGroup: (req: TransactionTagGroupDeleteRequest): ApiResponsePromise<boolean> => {
|
||||
return axios.post<ApiResponse<boolean>>('v1/transaction/tags/groups/delete.json', req);
|
||||
},
|
||||
getAllTransactionTags: (): ApiResponsePromise<TransactionTagInfoResponse[]> => {
|
||||
return axios.get<ApiResponse<TransactionTagInfoResponse[]>>('v1/transaction/tags/list.json');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Abfrageelemente dürfen nicht leer sein",
|
||||
"query items too much": "Zu viele Abfrageelemente",
|
||||
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Version",
|
||||
"Edit": "Bearbeiten",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Entfernen",
|
||||
"Delete": "Löschen",
|
||||
"Duplicate": "Duplizieren",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Versteckte Transaktionskategorien anzeigen",
|
||||
"Hide Hidden Transaction Categories": "Versteckte Transaktionskategorien ausblenden",
|
||||
"Transaction Tags": "Transaktions-Tags",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Tag-Titel",
|
||||
"No available tag": "Kein verfügbaren Tag",
|
||||
"Find tag": "Tag finden",
|
||||
"Unable to retrieve tag list": "Tag-Liste kann nicht abgerufen werden",
|
||||
"Tag list is up to date": "Tag-Liste ist aktuell",
|
||||
"Tag list has been updated": "Tag-Liste wurde aktualisiert",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Neuen Tag hinzufügen",
|
||||
"Unable to add tag": "Tag kann nicht hinzugefügt werden",
|
||||
"Unable to save tag": "Tag kann nicht gespeichert werden",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "There are no query items",
|
||||
"query items too much": "There are too many query items",
|
||||
"query items have invalid item": "There is invalid item in query items",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Version",
|
||||
"Edit": "Edit",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Remove",
|
||||
"Delete": "Delete",
|
||||
"Duplicate": "Duplicate",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Show Hidden Transaction Categories",
|
||||
"Hide Hidden Transaction Categories": "Hide Hidden Transaction Categories",
|
||||
"Transaction Tags": "Transaction Tags",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Tag Title",
|
||||
"No available tag": "No available tag",
|
||||
"Find tag": "Find tag",
|
||||
"Unable to retrieve tag list": "Unable to retrieve tag list",
|
||||
"Tag list is up to date": "Tag list is up to date",
|
||||
"Tag list has been updated": "Tag list has been updated",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Add new tag",
|
||||
"Unable to add tag": "Unable to add tag",
|
||||
"Unable to save tag": "Unable to save tag",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "There are no query items",
|
||||
"query items too much": "There are too many query items",
|
||||
"query items have invalid item": "Hay un elemento no válido en los elementos de consulta",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Versión",
|
||||
"Edit": "Editar",
|
||||
"View": "Ver",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Eliminar",
|
||||
"Delete": "Borrar",
|
||||
"Duplicate": "Duplicar",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Mostrar categorías de transacciones ocultas",
|
||||
"Hide Hidden Transaction Categories": "Ocultar categorías de transacciones ocultas",
|
||||
"Transaction Tags": "Etiquetas de Transacciones",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Título de la Etiqueta",
|
||||
"No available tag": "No hay etiquetas disponibles",
|
||||
"Find tag": "Find tag",
|
||||
"Unable to retrieve tag list": "No se puede recuperar la lista de etiquetas",
|
||||
"Tag list is up to date": "La lista de etiquetas está actualizada.",
|
||||
"Tag list has been updated": "La lista de etiquetas ha sido actualizada.",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Agregar nueva etiqueta",
|
||||
"Unable to add tag": "No se puede agregar la etiqueta",
|
||||
"Unable to save tag": "No se puede guardar la etiqueta",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Il n'y a pas d'éléments de requête",
|
||||
"query items too much": "Il y a trop d'éléments de requête",
|
||||
"query items have invalid item": "Il y a un élément invalide dans les éléments de requête",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Version",
|
||||
"Edit": "Modifier",
|
||||
"View": "Voir",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Supprimer",
|
||||
"Delete": "Supprimer",
|
||||
"Duplicate": "Dupliquer",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Afficher les catégories de transaction masquées",
|
||||
"Hide Hidden Transaction Categories": "Masquer les catégories de transaction masquées",
|
||||
"Transaction Tags": "Étiquettes de transaction",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Titre d'étiquette",
|
||||
"No available tag": "Aucune étiquette disponible",
|
||||
"Find tag": "Rechercher une étiquette",
|
||||
"Unable to retrieve tag list": "Impossible de récupérer la liste des étiquettes",
|
||||
"Tag list is up to date": "La liste des étiquettes est à jour",
|
||||
"Tag list has been updated": "La liste des étiquettes a été mise à jour",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Ajouter une nouvelle étiquette",
|
||||
"Unable to add tag": "Impossible d'ajouter l'étiquette",
|
||||
"Unable to save tag": "Impossible d'enregistrer l'étiquette",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Non ci sono elementi di query",
|
||||
"query items too much": "Ci sono troppi elementi di query",
|
||||
"query items have invalid item": "C'è un elemento non valido negli elementi di query",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Versione",
|
||||
"Edit": "Modifica",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Rimuovi",
|
||||
"Delete": "Elimina",
|
||||
"Duplicate": "Duplica",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Mostra categorie transazione nascoste",
|
||||
"Hide Hidden Transaction Categories": "Nascondi categorie transazione nascoste",
|
||||
"Transaction Tags": "Tag transazione",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Titolo tag",
|
||||
"No available tag": "Nessun tag disponibile",
|
||||
"Find tag": "Trova tag",
|
||||
"Unable to retrieve tag list": "Impossibile recuperare l'elenco dei tag",
|
||||
"Tag list is up to date": "Elenco tag aggiornato",
|
||||
"Tag list has been updated": "Elenco tag aggiornato",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Aggiungi nuovo tag",
|
||||
"Unable to add tag": "Impossibile aggiungere il tag",
|
||||
"Unable to save tag": "Impossibile salvare il tag",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "クエリ項目がありません",
|
||||
"query items too much": "クエリ項目が多すぎます",
|
||||
"query items have invalid item": "クエリ項目に無効な項目があります",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "バージョン",
|
||||
"Edit": "編集",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "削除",
|
||||
"Delete": "削除",
|
||||
"Duplicate": "複製",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "非表示取引カテゴリを表示",
|
||||
"Hide Hidden Transaction Categories": "非表示取引カテゴリを非表示",
|
||||
"Transaction Tags": "取引タグ",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "タグタイトル",
|
||||
"No available tag": "利用可能なタグはありません",
|
||||
"Find tag": "タグを見つける",
|
||||
"Unable to retrieve tag list": "タグリストを取得できません",
|
||||
"Tag list is up to date": "タグリストは最新です",
|
||||
"Tag list has been updated": "タグリストが更新されました",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "新しいタグを追加",
|
||||
"Unable to add tag": "タグを追加できません",
|
||||
"Unable to save tag": "タグを保存できません",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "ವಿಚರಣೆ ಐಟಂಗಳಿಲ್ಲ",
|
||||
"query items too much": "ವಿಚರಣೆ ಐಟಂಗಳ ಸಂಖ್ಯೆ ಹೆಚ್ಚು",
|
||||
"query items have invalid item": "ವಿಚರಣೆ ಐಟಂಗಳಲ್ಲಿ ಅಮಾನ್ಯ ಐಟಂ ಇದೆ",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "ಆವೃತ್ತಿ",
|
||||
"Edit": "ತಿದ್ದು",
|
||||
"View": "ನೋಡು",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "ತೆಗೆದುಹಾಕು",
|
||||
"Delete": "ಅಳಿಸು",
|
||||
"Duplicate": "ನಕಲು ಮಾಡು",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "ಮರೆಮಾಡಿದ ವಹಿವಾಟು ವರ್ಗಗಳನ್ನು ತೋರಿಸಿ",
|
||||
"Hide Hidden Transaction Categories": "ಮರೆಮಾಡಿದ ವಹಿವಾಟು ವರ್ಗಗಳನ್ನು ಮರೆಮಾಡಿ",
|
||||
"Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್ಗಳು",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "ಟ್ಯಾಗ್ ಶೀರ್ಷಿಕೆ",
|
||||
"No available tag": "ಯಾವುದೇ ಟ್ಯಾಗ್ ಲಭ್ಯವಿಲ್ಲ",
|
||||
"Find tag": "ಟ್ಯಾಗ್ ಹುಡುಕಿ",
|
||||
"Unable to retrieve tag list": "ಟ್ಯಾಗ್ ಪಟ್ಟಿ ಪಡೆಯಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
|
||||
"Tag list is up to date": "ಟ್ಯಾಗ್ ಪಟ್ಟಿ ನವೀಕೃತವಾಗಿದೆ",
|
||||
"Tag list has been updated": "ಟ್ಯಾಗ್ ಪಟ್ಟಿ ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "ಹೊಸ ಟ್ಯಾಗ್ ಸೇರಿಸಿ",
|
||||
"Unable to add tag": "ಟ್ಯಾಗ್ ಸೇರಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
|
||||
"Unable to save tag": "ಟ್ಯಾಗ್ ಉಳಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "쿼리 항목이 비어 있을 수 없습니다.",
|
||||
"query items too much": "쿼리 항목이 너무 많습니다.",
|
||||
"query items have invalid item": "쿼리 항목에 유효하지 않은 항목이 있습니다.",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "버전",
|
||||
"Edit": "편집",
|
||||
"View": "보기",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "제거",
|
||||
"Delete": "삭제",
|
||||
"Duplicate": "복제",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "숨겨진 거래 카테고리 표시",
|
||||
"Hide Hidden Transaction Categories": "숨겨진 거래 카테고리 숨기기",
|
||||
"Transaction Tags": "거래 태그",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "태그 제목",
|
||||
"No available tag": "사용 가능한 태그가 없습니다.",
|
||||
"Find tag": "태그 찾기",
|
||||
"Unable to retrieve tag list": "태그 목록을 가져올 수 없습니다.",
|
||||
"Tag list is up to date": "태그 목록이 최신 상태입니다.",
|
||||
"Tag list has been updated": "태그 목록이 업데이트되었습니다.",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "새 태그 추가",
|
||||
"Unable to add tag": "태그를 추가할 수 없습니다.",
|
||||
"Unable to save tag": "태그를 저장할 수 없습니다.",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Geen zoekitems opgegeven",
|
||||
"query items too much": "Te veel zoekitems",
|
||||
"query items have invalid item": "Ongeldig item in zoekitems",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Versie",
|
||||
"Edit": "Bewerken",
|
||||
"View": "Bekijken",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Verwijderen",
|
||||
"Delete": "Verwijderen",
|
||||
"Duplicate": "Dupliceren",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Verborgen transactiecategorieën tonen",
|
||||
"Hide Hidden Transaction Categories": "Verborgen transactiecategorieën verbergen",
|
||||
"Transaction Tags": "Transactietags",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Tagtitel",
|
||||
"No available tag": "Geen beschikbare tag",
|
||||
"Find tag": "Tag zoeken",
|
||||
"Unable to retrieve tag list": "Kan taglijst niet ophalen",
|
||||
"Tag list is up to date": "Taglijst is up-to-date",
|
||||
"Tag list has been updated": "Taglijst is bijgewerkt",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Nieuwe tag toevoegen",
|
||||
"Unable to add tag": "Kan tag niet toevoegen",
|
||||
"Unable to save tag": "Kan tag niet opslaan",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Não há itens de consulta",
|
||||
"query items too much": "Há muitos itens de consulta",
|
||||
"query items have invalid item": "Há item inválido nos itens de consulta",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Versão",
|
||||
"Edit": "Editar",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Remover",
|
||||
"Delete": "Excluir",
|
||||
"Duplicate": "Duplicar",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Mostrar Categorias de Transações Ocultas",
|
||||
"Hide Hidden Transaction Categories": "Ocultar Categorias de Transações Ocultas",
|
||||
"Transaction Tags": "Tags de Transação",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Título da Tag",
|
||||
"No available tag": "Nenhuma tag disponível",
|
||||
"Find tag": "Encontrar tag",
|
||||
"Unable to retrieve tag list": "Não foi possível recuperar a lista de tags",
|
||||
"Tag list is up to date": "A lista de tags está atualizada",
|
||||
"Tag list has been updated": "A lista de tags foi atualizada",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Adicionar nova tag",
|
||||
"Unable to add tag": "Não foi possível adicionar tag",
|
||||
"Unable to save tag": "Não foi possível salvar tag",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Нет элементов запроса",
|
||||
"query items too much": "Слишком много элементов запроса",
|
||||
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Версия",
|
||||
"Edit": "Редактировать",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Удалить",
|
||||
"Delete": "Удалить",
|
||||
"Duplicate": "Дублировать",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Показать скрытые категории транзакций",
|
||||
"Hide Hidden Transaction Categories": "Скрыть скрытые категории транзакций",
|
||||
"Transaction Tags": "Теги транзакций",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Название тега",
|
||||
"No available tag": "Нет доступных тегов",
|
||||
"Find tag": "Find tag",
|
||||
"Unable to retrieve tag list": "Не удалось получить список тегов",
|
||||
"Tag list is up to date": "Список тегов актуален",
|
||||
"Tag list has been updated": "Список тегов обновлен",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Добавить новый тег",
|
||||
"Unable to add tag": "Не удалось добавить тег",
|
||||
"Unable to save tag": "Не удалось сохранить тег",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Poizvedbeni elementi ne morejo biti prazni",
|
||||
"query items too much": "Preveč poizvedbenih elementov",
|
||||
"query items have invalid item": "Med poizvedbenimi elementi je neveljaven element",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Različica",
|
||||
"Edit": "Uredi",
|
||||
"View": "Ogled",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Odstrani",
|
||||
"Delete": "Izbriši",
|
||||
"Duplicate": "Podvoji",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Prikaži skrite kategorije transakcij",
|
||||
"Hide Hidden Transaction Categories": "Skrij skrite kategorije transakcij",
|
||||
"Transaction Tags": "Oznake transakcij",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Naslov oznake",
|
||||
"No available tag": "Ni razpoložljivih oznak",
|
||||
"Find tag": "Poišči oznako",
|
||||
"Unable to retrieve tag list": "Seznama oznak ni mogoče pridobiti",
|
||||
"Tag list is up to date": "Seznam oznak je posodobljen",
|
||||
"Tag list has been updated": "Seznam oznak je bil posodobljen",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Dodaj novo oznako",
|
||||
"Unable to add tag": "Oznake ni mogoče dodati",
|
||||
"Unable to save tag": "Oznake ni mogoče shraniti",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "ไม่มีรายการสำหรับค้นหา",
|
||||
"query items too much": "รายการค้นหามากเกินไป",
|
||||
"query items have invalid item": "มีรายการไม่ถูกต้องในรายการค้นหา",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "เวอร์ชัน",
|
||||
"Edit": "แก้ไข",
|
||||
"View": "ดู",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "เอาออก",
|
||||
"Delete": "ลบ",
|
||||
"Duplicate": "ทำสำเนา",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "แสดงหมวดรายการที่ซ่อนอยู่",
|
||||
"Hide Hidden Transaction Categories": "ซ่อนหมวดรายการที่ซ่อนอยู่",
|
||||
"Transaction Tags": "แท็กรายการ",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "ชื่อแท็ก",
|
||||
"No available tag": "ไม่มีแท็กที่ใช้ได้",
|
||||
"Find tag": "ค้นหาแท็ก",
|
||||
"Unable to retrieve tag list": "ไม่สามารถดึงรายการแท็กได้",
|
||||
"Tag list is up to date": "รายการแท็กเป็นปัจจุบันแล้ว",
|
||||
"Tag list has been updated": "รายการแท็กถูกอัปเดตแล้ว",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "เพิ่มแท็กใหม่",
|
||||
"Unable to add tag": "ไม่สามารถเพิ่มแท็กได้",
|
||||
"Unable to save tag": "ไม่สามารถบันทึกแท็กได้",
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@
|
|||
"ZMW": "Zambiya Kvaçası",
|
||||
"ZWG": "Zimbabve Altını",
|
||||
"ZWL": "Zimbabve Doları"
|
||||
},
|
||||
},
|
||||
"unit": {
|
||||
"Afghani": {
|
||||
"normal": "Afgani",
|
||||
|
|
@ -1053,7 +1053,7 @@
|
|||
"amap": "Amap",
|
||||
"custom": "Kullanıcı Tanımlı"
|
||||
},
|
||||
"error": {
|
||||
"error": {
|
||||
"system error": "Sistem Hatası",
|
||||
"api not found": "API isteği başarısız oldu",
|
||||
"not implemented": "Henüz uygulanmadı",
|
||||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Sorgu öğeleri boş olamaz",
|
||||
"query items too much": "Çok fazla sorgu öğesi var",
|
||||
"query items have invalid item": "Sorgu öğelerinde geçersiz öğe var",
|
||||
|
|
@ -1410,7 +1413,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sample": {
|
||||
"sample": {
|
||||
"importTransactionCustomScript": {
|
||||
"headerComment": "Örnek betik:",
|
||||
"functionDescription": "Ayrıştırılan dosya verisinin her satırı için parse fonksiyonu çağrılacaktır, fonksiyon adı 'parse' olmalıdır",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Sürüm",
|
||||
"Edit": "Düzenle",
|
||||
"View": "Görüntüle",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Kaldır",
|
||||
"Delete": "Sil",
|
||||
"Duplicate": "Çoğalt",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Gizli İşlem Kategorilerini Göster",
|
||||
"Hide Hidden Transaction Categories": "Gizli İşlem Kategorilerini Gizle",
|
||||
"Transaction Tags": "İşlem Etiketleri",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Etiket Başlığı",
|
||||
"No available tag": "Mevcut etiket yok",
|
||||
"Find tag": "Etiket bul",
|
||||
"Unable to retrieve tag list": "Etiket listesi alınamadı",
|
||||
"Tag list is up to date": "Etiket listesi güncel",
|
||||
"Tag list has been updated": "Etiket listesi güncellendi",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Yeni etiket ekle",
|
||||
"Unable to add tag": "Etiket eklenemedi",
|
||||
"Unable to save tag": "Etiket kaydedilemedi",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Елементи запиту не можуть бути порожніми",
|
||||
"query items too much": "Занадто багато елементів запиту",
|
||||
"query items have invalid item": "Запит містить недійсний елемент",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Версія",
|
||||
"Edit": "Редагувати",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Видалити",
|
||||
"Delete": "Видалити",
|
||||
"Duplicate": "Дублювати",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Показати приховані категорії транзакцій",
|
||||
"Hide Hidden Transaction Categories": "Приховати приховані категорії транзакцій",
|
||||
"Transaction Tags": "Теги транзакцій",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Назва тегу",
|
||||
"No available tag": "Немає доступних тегів",
|
||||
"Find tag": "Знайти тег",
|
||||
"Unable to retrieve tag list": "Не вдалося отримати список тегів",
|
||||
"Tag list is up to date": "Список тегів актуальний",
|
||||
"Tag list has been updated": "Список тегів оновлено",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Додати новий тег",
|
||||
"Unable to add tag": "Не вдалося додати тег",
|
||||
"Unable to save tag": "Не вдалося зберегти тег",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "Explorer ID is invalid",
|
||||
"explorer not found": "Explorer is not found",
|
||||
"explorer data is invalid": "Explorer data is invalid",
|
||||
"transaction tag group id is invalid": "Transaction tag group ID is invalid",
|
||||
"transaction tag group not found": "Transaction tag group is not found",
|
||||
"transaction tag group is in use and cannot be deleted": "Transaction tag group is in use and it cannot be deleted",
|
||||
"query items cannot be blank": "Không có mục truy vấn",
|
||||
"query items too much": "Có quá nhiều mục truy vấn",
|
||||
"query items have invalid item": "Có mục không hợp lệ trong các mục truy vấn",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "Phiên bản",
|
||||
"Edit": "Sửa",
|
||||
"View": "View",
|
||||
"Move": "Move",
|
||||
"Move to...": "Move to...",
|
||||
"Remove": "Xóa",
|
||||
"Delete": "Xóa",
|
||||
"Duplicate": "Nhân đôi",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "Hiển thị danh mục giao dịch ẩn",
|
||||
"Hide Hidden Transaction Categories": "Ẩn danh mục giao dịch ẩn",
|
||||
"Transaction Tags": "Thẻ giao dịch",
|
||||
"Total tags": "Total tags",
|
||||
"Default Group": "Default Group",
|
||||
"Tag Title": "Tiêu đề thẻ",
|
||||
"No available tag": "Không có thẻ nào khả dụng",
|
||||
"Find tag": "Find tag",
|
||||
"Unable to retrieve tag list": "Không thể truy xuất danh sách thẻ",
|
||||
"Tag list is up to date": "Danh sách thẻ đã được cập nhật",
|
||||
"Tag list has been updated": "Danh sách thẻ đã được cập nhật",
|
||||
"Add Tag Group": "Add Tag Group",
|
||||
"Rename Tag Group": "Rename Tag Group",
|
||||
"Delete Tag Group": "Delete Tag Group",
|
||||
"Change Group Display Order": "Change Group Display Order",
|
||||
"Transaction Tag Groups": "Transaction Tag Groups",
|
||||
"Tag Group Name": "Tag Group Name",
|
||||
"New Tag Group Name": "New Tag Group Name",
|
||||
"No available tag group": "No available tag group",
|
||||
"Unable to retrieve tag group list": "Unable to retrieve tag group list",
|
||||
"Tag group list has been updated": "Tag group list has been updated",
|
||||
"Unable to add tag group": "Unable to add tag group",
|
||||
"Unable to save tag group": "Unable to save tag group",
|
||||
"Unable to move tag group": "Unable to move tag group",
|
||||
"Unable to rename this tag group": "Unable to rename this tag group",
|
||||
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?",
|
||||
"Unable to delete this tag group": "Unable to delete this tag group",
|
||||
"Add new tag": "Add new tag",
|
||||
"Unable to add tag": "Không thể thêm thẻ",
|
||||
"Unable to save tag": "Không thể lưu thẻ",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "探索ID无效",
|
||||
"explorer not found": "探索不存在",
|
||||
"explorer data is invalid": "探索数据无效",
|
||||
"transaction tag group id is invalid": "交易标签组ID无效",
|
||||
"transaction tag group not found": "交易标签组不存在",
|
||||
"transaction tag group is in use and cannot be deleted": "交易标签组正在被使用,无法删除",
|
||||
"query items cannot be blank": "请求项目不能为空",
|
||||
"query items too much": "请求项目过多",
|
||||
"query items have invalid item": "请求项目中有非法项目",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "版本",
|
||||
"Edit": "编辑",
|
||||
"View": "查看",
|
||||
"Move": "移动",
|
||||
"Move to...": "移动到...",
|
||||
"Remove": "移除",
|
||||
"Delete": "删除",
|
||||
"Duplicate": "复制",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "显示隐藏的交易分类",
|
||||
"Hide Hidden Transaction Categories": "不显示隐藏的交易分类",
|
||||
"Transaction Tags": "交易标签",
|
||||
"Total tags": "总标签数",
|
||||
"Default Group": "默认分组",
|
||||
"Tag Title": "标签标题",
|
||||
"No available tag": "没有可用的标签",
|
||||
"Find tag": "查找标签",
|
||||
"Unable to retrieve tag list": "无法获取标签列表",
|
||||
"Tag list is up to date": "标签列表已是最新",
|
||||
"Tag list has been updated": "标签列表已更新",
|
||||
"Add Tag Group": "添加标签组",
|
||||
"Rename Tag Group": "重命名标签组",
|
||||
"Delete Tag Group": "删除标签组",
|
||||
"Change Group Display Order": "更改分组显示顺序",
|
||||
"Transaction Tag Groups": "交易标签组",
|
||||
"Tag Group Name": "标签组名称",
|
||||
"New Tag Group Name": "新标签组名称",
|
||||
"No available tag group": "没有可用的标签组",
|
||||
"Unable to retrieve tag group list": "无法获取标签组列表",
|
||||
"Tag group list has been updated": "标签组列表已更新",
|
||||
"Unable to add tag group": "无法添加标签组",
|
||||
"Unable to save tag group": "无法保存标签组",
|
||||
"Unable to move tag group": "无法移动标签组",
|
||||
"Unable to rename this tag group": "无法重命名该标签组",
|
||||
"Are you sure you want to delete this tag group?": "您确定要删除该标签组?",
|
||||
"Unable to delete this tag group": "无法删除该标签组",
|
||||
"Add new tag": "添加新标签",
|
||||
"Unable to add tag": "无法添加标签",
|
||||
"Unable to save tag": "无法保存标签",
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,9 @@
|
|||
"explorer id is invalid": "探索ID無效",
|
||||
"explorer not found": "探索不存在",
|
||||
"explorer data is invalid": "探索資料無效",
|
||||
"transaction tag group id is invalid": "交易標籤組ID無效",
|
||||
"transaction tag group not found": "交易標籤組不存在",
|
||||
"transaction tag group is in use and cannot be deleted": "交易標籤組正在被使用,無法刪除",
|
||||
"query items cannot be blank": "查詢項目不能為空",
|
||||
"query items too much": "查詢項目過多",
|
||||
"query items have invalid item": "查詢項目中有非法項目",
|
||||
|
|
@ -1485,6 +1488,8 @@
|
|||
"Version": "版本",
|
||||
"Edit": "編輯",
|
||||
"View": "檢視",
|
||||
"Move": "移動",
|
||||
"Move to...": "移動到...",
|
||||
"Remove": "移除",
|
||||
"Delete": "刪除",
|
||||
"Duplicate": "複製",
|
||||
|
|
@ -2376,12 +2381,30 @@
|
|||
"Show Hidden Transaction Categories": "顯示隱藏的交易分類",
|
||||
"Hide Hidden Transaction Categories": "不顯示隱藏的交易分類",
|
||||
"Transaction Tags": "交易標籤",
|
||||
"Total tags": "總標籤數",
|
||||
"Default Group": "預設群組",
|
||||
"Tag Title": "標籤標題",
|
||||
"No available tag": "沒有可用的標籤",
|
||||
"Find tag": "尋找標籤",
|
||||
"Unable to retrieve tag list": "無法取得標籤清單",
|
||||
"Tag list is up to date": "標籤清單已是最新",
|
||||
"Tag list has been updated": "標籤清單已更新",
|
||||
"Add Tag Group": "新增標籤群組",
|
||||
"Rename Tag Group": "重新命名標籤群組",
|
||||
"Delete Tag Group": "刪除標籤群組",
|
||||
"Change Group Display Order": "變更群組顯示順序",
|
||||
"Transaction Tag Groups": "交易標籤群組",
|
||||
"Tag Group Name": "標籤群組名稱",
|
||||
"New Tag Group Name": "新標籤群組名稱",
|
||||
"No available tag group": "沒有可用的標籤群組",
|
||||
"Unable to retrieve tag group list": "無法取得標籤群組清單",
|
||||
"Tag group list has been updated": "標籤群組清單已更新",
|
||||
"Unable to add tag group": "無法新增標籤群組",
|
||||
"Unable to save tag group": "無法儲存標籤群組",
|
||||
"Unable to move tag group": "無法移動標籤群組",
|
||||
"Unable to rename this tag group": "無法重新命名此標籤群組",
|
||||
"Are you sure you want to delete this tag group?": "您確定要刪除此標籤群組?",
|
||||
"Unable to delete this tag group": "無法刪除此標籤群組",
|
||||
"Add new tag": "新增標籤",
|
||||
"Unable to add tag": "無法新增標籤",
|
||||
"Unable to save tag": "無法儲存標籤",
|
||||
|
|
|
|||
|
|
@ -448,8 +448,8 @@ export class TransactionTagFilter {
|
|||
this.type = type;
|
||||
}
|
||||
|
||||
public static create(type: TransactionTagFilterType): TransactionTagFilter {
|
||||
return new TransactionTagFilter([], type);
|
||||
public static create(tagIds: string[], type: TransactionTagFilterType): TransactionTagFilter {
|
||||
return new TransactionTagFilter(tagIds, type);
|
||||
}
|
||||
|
||||
public static of(tagId: string): TransactionTagFilter {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,39 @@
|
|||
export class TransactionTag implements TransactionTagInfoResponse {
|
||||
public id: string;
|
||||
public name: string;
|
||||
public groupId: string;
|
||||
public displayOrder: number;
|
||||
public hidden: boolean;
|
||||
|
||||
private constructor(id: string, name: string, displayOrder: number, hidden: boolean) {
|
||||
private constructor(id: string, name: string, groupId: string, displayOrder: number, hidden: boolean) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.groupId = groupId;
|
||||
this.displayOrder = displayOrder;
|
||||
this.hidden = hidden;
|
||||
}
|
||||
|
||||
public toCreateRequest(): TransactionTagCreateRequest {
|
||||
return {
|
||||
name: this.name
|
||||
name: this.name,
|
||||
groupId: this.groupId
|
||||
};
|
||||
}
|
||||
|
||||
public toModifyRequest(): TransactionTagModifyRequest {
|
||||
return {
|
||||
id: this.id,
|
||||
groupId: this.groupId,
|
||||
name: this.name
|
||||
};
|
||||
}
|
||||
|
||||
public clone(): TransactionTag {
|
||||
return new TransactionTag(this.id, this.name, this.groupId, this.displayOrder, this.hidden);
|
||||
}
|
||||
|
||||
public static of(tagResponse: TransactionTagInfoResponse): TransactionTag {
|
||||
return new TransactionTag(tagResponse.id, tagResponse.name, tagResponse.displayOrder, tagResponse.hidden);
|
||||
return new TransactionTag(tagResponse.id, tagResponse.name, tagResponse.groupId, tagResponse.displayOrder, tagResponse.hidden);
|
||||
}
|
||||
|
||||
public static ofMulti(tagResponses: TransactionTagInfoResponse[]): TransactionTag[] {
|
||||
|
|
@ -38,22 +46,25 @@ export class TransactionTag implements TransactionTagInfoResponse {
|
|||
return tags;
|
||||
}
|
||||
|
||||
public static createNewTag(name?: string): TransactionTag {
|
||||
return new TransactionTag('', name || '', 0, false);
|
||||
public static createNewTag(name?: string, groupId?: string): TransactionTag {
|
||||
return new TransactionTag('', name || '', groupId || '0', 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransactionTagCreateRequest {
|
||||
readonly groupId: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface TransactionTagCreateBatchRequest {
|
||||
readonly tags: TransactionTagCreateRequest[];
|
||||
readonly groupId: string;
|
||||
readonly skipExists: boolean;
|
||||
}
|
||||
|
||||
export interface TransactionTagModifyRequest {
|
||||
readonly id: string;
|
||||
readonly groupId: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +89,7 @@ export interface TransactionTagDeleteRequest {
|
|||
export interface TransactionTagInfoResponse {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly groupId: string;
|
||||
readonly displayOrder: number;
|
||||
readonly hidden: boolean;
|
||||
}
|
||||
|
|
|
|||
74
src/models/transaction_tag_group.ts
Normal file
74
src/models/transaction_tag_group.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
export class TransactionTagGroup implements TransactionTagGroupInfoResponse {
|
||||
public id: string;
|
||||
public name: string;
|
||||
public displayOrder: number;
|
||||
|
||||
private constructor(id: string, name: string, displayOrder: number) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.displayOrder = displayOrder;
|
||||
}
|
||||
|
||||
public toCreateRequest(): TransactionTagGroupCreateRequest {
|
||||
return {
|
||||
name: this.name
|
||||
};
|
||||
}
|
||||
|
||||
public toModifyRequest(): TransactionTagGroupModifyRequest {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name
|
||||
};
|
||||
}
|
||||
|
||||
public clone(): TransactionTagGroup {
|
||||
return new TransactionTagGroup(this.id, this.name, this.displayOrder);
|
||||
}
|
||||
|
||||
public static of(tagGroupResponse: TransactionTagGroupInfoResponse): TransactionTagGroup {
|
||||
return new TransactionTagGroup(tagGroupResponse.id, tagGroupResponse.name, tagGroupResponse.displayOrder);
|
||||
}
|
||||
|
||||
public static ofMulti(tagGroupResponses: TransactionTagGroupInfoResponse[]): TransactionTagGroup[] {
|
||||
const tagGroups: TransactionTagGroup[] = [];
|
||||
|
||||
for (const tagGroupResponse of tagGroupResponses) {
|
||||
tagGroups.push(TransactionTagGroup.of(tagGroupResponse));
|
||||
}
|
||||
|
||||
return tagGroups;
|
||||
}
|
||||
|
||||
public static createNewTagGroup(name?: string): TransactionTagGroup {
|
||||
return new TransactionTagGroup('', name || '', 0);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupCreateRequest {
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupModifyRequest {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupMoveRequest {
|
||||
readonly newDisplayOrders: TransactionTagGroupNewDisplayOrderRequest[];
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupNewDisplayOrderRequest {
|
||||
readonly id: string;
|
||||
readonly displayOrder: number;
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupDeleteRequest {
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface TransactionTagGroupInfoResponse {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly displayOrder: number;
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
import { ref, computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { type BeforeResolveFunction, itemAndIndex } from '@/core/base.ts';
|
||||
import { type BeforeResolveFunction, itemAndIndex, values } from '@/core/base.ts';
|
||||
|
||||
import {
|
||||
type TransactionTagGroupInfoResponse,
|
||||
type TransactionTagGroupNewDisplayOrderRequest,
|
||||
TransactionTagGroup
|
||||
} from '@/models/transaction_tag_group.ts';
|
||||
|
||||
import {
|
||||
type TransactionTagCreateBatchRequest,
|
||||
|
|
@ -16,52 +22,176 @@ import logger from '@/lib/logger.ts';
|
|||
import services, { type ApiResponsePromise } from '@/lib/services.ts';
|
||||
|
||||
export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
||||
const allTransactionTagGroups = ref<TransactionTagGroup[]>([]);
|
||||
const allTransactionTagGroupsMap = ref<Record<string, TransactionTagGroup>>({});
|
||||
const allTransactionTags = ref<TransactionTag[]>([]);
|
||||
const allTransactionTagsMap = ref<Record<string, TransactionTag>>({});
|
||||
const allTransactionTagsByGroupMap = ref<Record<string, TransactionTag[]>>({});
|
||||
const transactionTagGroupListStateInvalid = ref<boolean>(true);
|
||||
const transactionTagListStateInvalid = ref<boolean>(true);
|
||||
|
||||
const allVisibleTags = computed<TransactionTag[]>(() => {
|
||||
const visibleTags: TransactionTag[] = [];
|
||||
|
||||
for (const tag of allTransactionTags.value) {
|
||||
if (!tag.hidden) {
|
||||
visibleTags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return visibleTags;
|
||||
});
|
||||
|
||||
const allAvailableTagsCount = computed<number>(() => allTransactionTags.value.length);
|
||||
const allVisibleTagsCount = computed<number>(() => allVisibleTags.value.length);
|
||||
|
||||
function loadTransactionTagGroupList(tagGroups: TransactionTagGroup[]): void {
|
||||
allTransactionTagGroups.value = tagGroups;
|
||||
allTransactionTagGroupsMap.value = {};
|
||||
|
||||
for (const tagGroup of tagGroups) {
|
||||
allTransactionTagGroupsMap.value[tagGroup.id] = tagGroup;
|
||||
}
|
||||
}
|
||||
|
||||
function loadTransactionTagList(tags: TransactionTag[]): void {
|
||||
allTransactionTags.value = tags;
|
||||
allTransactionTagsMap.value = {};
|
||||
allTransactionTagsByGroupMap.value = {};
|
||||
|
||||
for (const tag of tags) {
|
||||
allTransactionTagsMap.value[tag.id] = tag;
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
let tagsInGroup = allTransactionTagsByGroupMap.value[tag.groupId];
|
||||
|
||||
if (!tagsInGroup) {
|
||||
tagsInGroup = [];
|
||||
allTransactionTagsByGroupMap.value[tag.groupId] = tagsInGroup;
|
||||
}
|
||||
|
||||
tagsInGroup.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
function addTagGroupToTransactionTagGroupList(tagGroup: TransactionTagGroup): void {
|
||||
allTransactionTagGroups.value.push(tagGroup);
|
||||
allTransactionTagGroupsMap.value[tagGroup.id] = tagGroup;
|
||||
}
|
||||
|
||||
function addTagToTransactionTagList(tag: TransactionTag): void {
|
||||
allTransactionTags.value.push(tag);
|
||||
allTransactionTagsMap.value[tag.id] = tag;
|
||||
|
||||
let tagsInGroup = allTransactionTagsByGroupMap.value[tag.groupId];
|
||||
|
||||
if (!tagsInGroup) {
|
||||
tagsInGroup = [];
|
||||
allTransactionTagsByGroupMap.value[tag.groupId] = tagsInGroup;
|
||||
}
|
||||
|
||||
tagsInGroup.push(tag);
|
||||
}
|
||||
|
||||
function updateTagInTransactionTagList(currentTag: TransactionTag): void {
|
||||
for (const [transactionTag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
if (transactionTag.id === currentTag.id) {
|
||||
allTransactionTags.value.splice(index, 1, currentTag);
|
||||
function updateTagGroupInTransactionTagGroupList(currentTagGroup: TransactionTagGroup): void {
|
||||
for (const [transactionTagGroup, index] of itemAndIndex(allTransactionTagGroups.value)) {
|
||||
if (transactionTagGroup.id === currentTagGroup.id) {
|
||||
allTransactionTagGroups.value.splice(index, 1, currentTagGroup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
allTransactionTagsMap.value[currentTag.id] = currentTag;
|
||||
allTransactionTagGroupsMap.value[currentTagGroup.id] = currentTagGroup;
|
||||
}
|
||||
|
||||
function updateTagDisplayOrderInTransactionTagList({ from, to }: { from: number, to: number }): void {
|
||||
allTransactionTags.value.splice(to, 0, allTransactionTags.value.splice(from, 1)[0] as TransactionTag);
|
||||
function updateTagInTransactionTagList(currentTag: TransactionTag, oldTagGroupId?: string): void {
|
||||
// update in the main list
|
||||
for (const [transactionTag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
if (transactionTag.id === currentTag.id) {
|
||||
if (oldTagGroupId && oldTagGroupId !== currentTag.groupId) {
|
||||
allTransactionTags.value.splice(index, 1);
|
||||
} else {
|
||||
allTransactionTags.value.splice(index, 1, currentTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldTagGroupId && oldTagGroupId !== currentTag.groupId) {
|
||||
let insertIndex = allTransactionTags.value.length;
|
||||
|
||||
for (const [tag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
if (tag.groupId === currentTag.groupId) {
|
||||
insertIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
allTransactionTags.value.splice(insertIndex, 0, currentTag);
|
||||
}
|
||||
|
||||
// update in the map
|
||||
allTransactionTagsMap.value[currentTag.id] = currentTag;
|
||||
|
||||
// update in the group list
|
||||
for (const tags of values(allTransactionTagsByGroupMap.value)) {
|
||||
for (const [transactionTag, index] of itemAndIndex(tags)) {
|
||||
if (transactionTag.id === currentTag.id) {
|
||||
if (oldTagGroupId && oldTagGroupId !== currentTag.groupId) {
|
||||
tags.splice(index, 1);
|
||||
} else {
|
||||
tags.splice(index, 1, currentTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldTagGroupId && oldTagGroupId !== currentTag.groupId) {
|
||||
let newGroupTags = allTransactionTagsByGroupMap.value[currentTag.groupId];
|
||||
|
||||
if (!newGroupTags) {
|
||||
newGroupTags = [];
|
||||
allTransactionTagsByGroupMap.value[currentTag.groupId] = newGroupTags;
|
||||
}
|
||||
|
||||
newGroupTags.push(currentTag);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTagGroupDisplayOrderInTransactionTagList({ from, to }: { from: number, to: number }): void {
|
||||
allTransactionTagGroups.value.splice(to, 0, allTransactionTagGroups.value.splice(from, 1)[0] as TransactionTagGroup);
|
||||
}
|
||||
|
||||
function updateTagDisplayOrderInTransactionTagList({ groupId, from, to }: { groupId: string, from: number, to: number }): void {
|
||||
// update in the group list
|
||||
const tagsInGroup = allTransactionTagsByGroupMap.value[groupId];
|
||||
|
||||
if (!tagsInGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromTag = tagsInGroup[from];
|
||||
|
||||
if (!fromTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toTag = tagsInGroup[to];
|
||||
|
||||
if (!toTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagsInGroup.splice(to, 0, tagsInGroup.splice(from, 1)[0] as TransactionTag);
|
||||
|
||||
// update in the main list
|
||||
let mainListFromIndex = -1;
|
||||
let mainListToIndex = -1;
|
||||
|
||||
for (const [tag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
if (tag.id === fromTag.id) {
|
||||
mainListFromIndex = index;
|
||||
}
|
||||
|
||||
if (tag.id === toTag.id) {
|
||||
mainListToIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainListFromIndex === -1 || mainListToIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
allTransactionTags.value.splice(mainListToIndex, 0, allTransactionTags.value.splice(mainListFromIndex, 1)[0] as TransactionTag);
|
||||
}
|
||||
|
||||
function updateTagVisibilityInTransactionTagList({ tag, hidden }: { tag: TransactionTag, hidden: boolean }): void {
|
||||
|
|
@ -70,6 +200,19 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function removeTagGroupFromTransactionTagGroupList(currentTagGroup: TransactionTagGroup): void {
|
||||
for (const [transactionTagGroup, index] of itemAndIndex(allTransactionTagGroups.value)) {
|
||||
if (transactionTagGroup.id === currentTagGroup.id) {
|
||||
allTransactionTagGroups.value.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allTransactionTagGroupsMap.value[currentTagGroup.id]) {
|
||||
delete allTransactionTagGroupsMap.value[currentTagGroup.id];
|
||||
}
|
||||
}
|
||||
|
||||
function removeTagFromTransactionTagList(currentTag: TransactionTag): void {
|
||||
for (const [transactionTag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
if (transactionTag.id === currentTag.id) {
|
||||
|
|
@ -81,6 +224,19 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
if (allTransactionTagsMap.value[currentTag.id]) {
|
||||
delete allTransactionTagsMap.value[currentTag.id];
|
||||
}
|
||||
|
||||
for (const tags of values(allTransactionTagsByGroupMap.value)) {
|
||||
for (const [transactionTag, index] of itemAndIndex(tags)) {
|
||||
if (transactionTag.id === currentTag.id) {
|
||||
tags.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTagGroupListInvalidState(invalidState: boolean): void {
|
||||
transactionTagGroupListStateInvalid.value = invalidState;
|
||||
}
|
||||
|
||||
function updateTransactionTagListInvalidState(invalidState: boolean): void {
|
||||
|
|
@ -88,20 +244,85 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
}
|
||||
|
||||
function resetTransactionTags(): void {
|
||||
allTransactionTagGroups.value = [];
|
||||
allTransactionTagGroupsMap.value = {};
|
||||
allTransactionTags.value = [];
|
||||
allTransactionTagsMap.value = {};
|
||||
allTransactionTagsByGroupMap.value = {};
|
||||
transactionTagGroupListStateInvalid.value = true;
|
||||
transactionTagListStateInvalid.value = true;
|
||||
}
|
||||
|
||||
function loadAllTagGroups({ force }: { force?: boolean }): Promise<TransactionTagGroup[]> {
|
||||
if (!force && !transactionTagGroupListStateInvalid.value) {
|
||||
return new Promise((resolve) => {
|
||||
resolve(allTransactionTagGroups.value);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getAllTransactionTagGroups().then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to retrieve tag group list' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionTagGroupListStateInvalid.value) {
|
||||
updateTransactionTagGroupListInvalidState(false);
|
||||
}
|
||||
|
||||
const transactionTagGroups = TransactionTagGroup.ofMulti(data.result);
|
||||
|
||||
loadTransactionTagGroupList(transactionTagGroups);
|
||||
|
||||
resolve(transactionTagGroups);
|
||||
}).catch(error => {
|
||||
logger.error('failed to load tag group list', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to retrieve tag group list' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadAllTags({ force }: { force?: boolean }): Promise<TransactionTag[]> {
|
||||
if (!force && !transactionTagListStateInvalid.value) {
|
||||
if (!force && !transactionTagGroupListStateInvalid.value && !transactionTagListStateInvalid.value) {
|
||||
return new Promise((resolve) => {
|
||||
resolve(allTransactionTags.value);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getAllTransactionTags().then(response => {
|
||||
services.getAllTransactionTagGroups().then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to retrieve tag list' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionTagGroupListStateInvalid.value) {
|
||||
updateTransactionTagGroupListInvalidState(false);
|
||||
}
|
||||
|
||||
const transactionTagGroups = TransactionTagGroup.ofMulti(data.result);
|
||||
|
||||
loadTransactionTagGroupList(transactionTagGroups);
|
||||
|
||||
return services.getAllTransactionTags();
|
||||
}).then(response => {
|
||||
if (!response) {
|
||||
reject({ message: 'Unable to retrieve tag list' });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
|
|
@ -141,7 +362,159 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
});
|
||||
}
|
||||
|
||||
function saveTagGroup({ tagGroup }: { tagGroup: TransactionTagGroup }): Promise<TransactionTagGroup> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let promise: ApiResponsePromise<TransactionTagGroupInfoResponse>;
|
||||
|
||||
if (!tagGroup.id) {
|
||||
promise = services.addTransactionTagGroup(tagGroup.toCreateRequest());
|
||||
} else {
|
||||
promise = services.modifyTransactionTagGroup(tagGroup.toModifyRequest());
|
||||
}
|
||||
|
||||
promise.then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
if (!tagGroup.id) {
|
||||
reject({ message: 'Unable to add tag group' });
|
||||
} else {
|
||||
reject({ message: 'Unable to save tag group' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const transactionTagGroup = TransactionTagGroup.of(data.result);
|
||||
|
||||
if (!tagGroup.id) {
|
||||
addTagGroupToTransactionTagGroupList(transactionTagGroup);
|
||||
} else {
|
||||
updateTagGroupInTransactionTagGroupList(transactionTagGroup);
|
||||
}
|
||||
|
||||
resolve(transactionTagGroup);
|
||||
}).catch(error => {
|
||||
logger.error('failed to save tag group', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
if (!tagGroup.id) {
|
||||
reject({ message: 'Unable to add tag group' });
|
||||
} else {
|
||||
reject({ message: 'Unable to save tag group' });
|
||||
}
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changeTagGroupDisplayOrder({ tagGroupId, from, to }: { tagGroupId: string, from: number, to: number }): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let currentTagGroup: TransactionTagGroup | null = null;
|
||||
|
||||
for (const transactionTagGroup of allTransactionTagGroups.value) {
|
||||
if (transactionTagGroup.id === tagGroupId) {
|
||||
currentTagGroup = transactionTagGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentTagGroup || !allTransactionTagGroups.value[to]) {
|
||||
reject({ message: 'Unable to move tag group' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transactionTagGroupListStateInvalid.value) {
|
||||
updateTransactionTagGroupListInvalidState(true);
|
||||
}
|
||||
|
||||
updateTagGroupDisplayOrderInTransactionTagList({ from, to });
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function updateTagGroupDisplayOrders(): Promise<boolean> {
|
||||
const newDisplayOrders: TransactionTagGroupNewDisplayOrderRequest[] = [];
|
||||
|
||||
for (const [transactionTagGroup, index] of itemAndIndex(allTransactionTagGroups.value)) {
|
||||
newDisplayOrders.push({
|
||||
id: transactionTagGroup.id,
|
||||
displayOrder: index + 1
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.moveTransactionTagGroup({
|
||||
newDisplayOrders: newDisplayOrders
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to move tag group' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionTagGroupListStateInvalid.value) {
|
||||
updateTransactionTagGroupListInvalidState(false);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to save tag groups display order', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to move tag group' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTagGroup({ tagGroup, beforeResolve }: { tagGroup: TransactionTagGroup, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.deleteTransactionTagGroup({
|
||||
id: tagGroup.id
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to delete this tag group' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (beforeResolve) {
|
||||
beforeResolve(() => {
|
||||
removeTagGroupFromTransactionTagGroupList(tagGroup);
|
||||
});
|
||||
} else {
|
||||
removeTagGroupFromTransactionTagGroupList(tagGroup);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to delete tag group', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to delete this tag group' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveTag({ tag }: { tag: TransactionTag }): Promise<TransactionTag> {
|
||||
const oldTagGroupId = allTransactionTagsMap.value[tag.id]?.groupId;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let promise: ApiResponsePromise<TransactionTagInfoResponse>;
|
||||
|
||||
|
|
@ -168,7 +541,7 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
if (!tag.id) {
|
||||
addTagToTransactionTagList(transactionTag);
|
||||
} else {
|
||||
updateTagInTransactionTagList(transactionTag);
|
||||
updateTagInTransactionTagList(transactionTag, oldTagGroupId);
|
||||
}
|
||||
|
||||
resolve(transactionTag);
|
||||
|
|
@ -241,16 +614,26 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
updateTransactionTagListInvalidState(true);
|
||||
}
|
||||
|
||||
updateTagDisplayOrderInTransactionTagList({ from, to });
|
||||
updateTagDisplayOrderInTransactionTagList({
|
||||
groupId: currentTag.groupId,
|
||||
from: from,
|
||||
to: to
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function updateTagDisplayOrders(): Promise<boolean> {
|
||||
function updateTagDisplayOrders(groupId: string): Promise<boolean> {
|
||||
const tagsInGroup = allTransactionTagsByGroupMap.value[groupId];
|
||||
|
||||
if (!tagsInGroup) {
|
||||
return Promise.reject('Unable to move tag');
|
||||
}
|
||||
|
||||
const newDisplayOrders: TransactionTagNewDisplayOrderRequest[] = [];
|
||||
|
||||
for (const [transactionTag, index] of itemAndIndex(allTransactionTags.value)) {
|
||||
for (const [transactionTag, index] of itemAndIndex(tagsInGroup)) {
|
||||
newDisplayOrders.push({
|
||||
id: transactionTag.id,
|
||||
displayOrder: index + 1
|
||||
|
|
@ -362,17 +745,24 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => {
|
|||
|
||||
return {
|
||||
// states
|
||||
allTransactionTagGroups,
|
||||
allTransactionTagGroupsMap,
|
||||
allTransactionTags,
|
||||
allTransactionTagsMap,
|
||||
allTransactionTagsByGroupMap,
|
||||
transactionTagGroupListStateInvalid,
|
||||
transactionTagListStateInvalid,
|
||||
// computed states
|
||||
allVisibleTags,
|
||||
allAvailableTagsCount,
|
||||
allVisibleTagsCount,
|
||||
// functions
|
||||
updateTransactionTagListInvalidState,
|
||||
resetTransactionTags,
|
||||
loadAllTagGroups,
|
||||
loadAllTags,
|
||||
saveTagGroup,
|
||||
changeTagGroupDisplayOrder,
|
||||
updateTagGroupDisplayOrders,
|
||||
deleteTagGroup,
|
||||
saveTag,
|
||||
addTags,
|
||||
changeTagDisplayOrder,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
84
src/views/base/tags/TagListPageBase.ts
Normal file
84
src/views/base/tags/TagListPageBase.ts
Normal file
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue