diff --git a/cmd/webserver.go b/cmd/webserver.go index 08bfb4df..e7b783c5 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -340,6 +340,7 @@ func startWebServer(c *core.CliContext) error { apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler)) apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler)) apiV1Route.POST("/transaction/tags/add.json", bindApi(api.TransactionTags.TagCreateHandler)) + apiV1Route.POST("/transaction/tags/add_batch.json", bindApi(api.TransactionTags.TagCreateBatchHandler)) apiV1Route.POST("/transaction/tags/modify.json", bindApi(api.TransactionTags.TagModifyHandler)) apiV1Route.POST("/transaction/tags/hide.json", bindApi(api.TransactionTags.TagHideHandler)) apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler)) diff --git a/pkg/api/transaction_tags.go b/pkg/api/transaction_tags.go index 1d87d913..0a9fba3f 100644 --- a/pkg/api/transaction_tags.go +++ b/pkg/api/transaction_tags.go @@ -101,6 +101,47 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er return tagResp, nil } +// TagCreateBatchHandler saves some new transaction tags by request parameters for current user +func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *errs.Error) { + var tagCreateBatchReq models.TransactionTagCreateBatchRequest + err := c.ShouldBindJSON(&tagCreateBatchReq) + + if err != nil { + log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + + maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid) + + if err != nil { + log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + tags := a.createNewTagModels(uid, &tagCreateBatchReq, maxOrderId+1) + + err = a.tags.CreateTags(c, uid, tags, tagCreateBatchReq.SkipExists) + + if err != nil { + log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to create tags for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[transaction_tags.TagCreateBatchHandler] user \"uid:%d\" has created tags successfully", uid) + + tagResps := make(models.TransactionTagInfoResponseSlice, len(tags)) + + for i := 0; i < len(tags); i++ { + tagResps[i] = tags[i].ToTransactionTagInfoResponse() + } + + sort.Sort(tagResps) + + return tagResps, nil +} + // TagModifyHandler saves an existed transaction tag by request parameters for current user func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Error) { var tagModifyReq models.TransactionTagModifyRequest @@ -230,3 +271,15 @@ func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.T DisplayOrder: order, } } + +func (a *TransactionTagsApi) createNewTagModels(uid int64, tagCreateBatchReq *models.TransactionTagCreateBatchRequest, order int32) []*models.TransactionTag { + tags := make([]*models.TransactionTag, len(tagCreateBatchReq.Tags)) + + for i := 0; i < len(tagCreateBatchReq.Tags); i++ { + tagCreateReq := tagCreateBatchReq.Tags[i] + tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i)) + tags[i] = tag + } + + return tags +} diff --git a/pkg/models/transaction_tag.go b/pkg/models/transaction_tag.go index 9369fe76..863eeefd 100644 --- a/pkg/models/transaction_tag.go +++ b/pkg/models/transaction_tag.go @@ -23,6 +23,12 @@ type TransactionTagCreateRequest struct { 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"` + 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"` @@ -59,6 +65,19 @@ type TransactionTagInfoResponse struct { Hidden bool `json:"hidden"` } +// FillFromOtherTag fills all the fields in this current tag from other transaction tag +func (t *TransactionTag) FillFromOtherTag(tag *TransactionTag) { + t.TagId = tag.TagId + t.Uid = tag.Uid + t.Deleted = tag.Deleted + t.Name = tag.Name + t.DisplayOrder = tag.DisplayOrder + t.Hidden = tag.Hidden + t.CreatedUnixTime = tag.CreatedUnixTime + t.UpdatedUnixTime = tag.UpdatedUnixTime + t.DeletedUnixTime = tag.DeletedUnixTime +} + // ToTransactionTagInfoResponse returns a view-object according to database model func (t *TransactionTag) ToTransactionTagInfoResponse() *TransactionTagInfoResponse { return &TransactionTagInfoResponse{ diff --git a/pkg/services/transaction_tags.go b/pkg/services/transaction_tags.go index 5a753af3..5527c1f7 100644 --- a/pkg/services/transaction_tags.go +++ b/pkg/services/transaction_tags.go @@ -222,6 +222,76 @@ func (s *TransactionTagService) CreateTag(c core.Context, tag *models.Transactio }) } +// CreateTags saves a few transaction tag models to database +func (s *TransactionTagService) CreateTags(c core.Context, uid int64, tags []*models.TransactionTag, skipExists bool) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + allTagNames := make([]string, len(tags)) + + for i := 0; i < len(tags); i++ { + allTagNames[i] = tags[i].Name + } + + var existTags []*models.TransactionTag + err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("name", allTagNames).Find(&existTags) + + if err != nil { + return err + } else if !skipExists && len(existTags) > 0 { + return errs.ErrTransactionTagNameAlreadyExists + } + + existsNameTagMap := make(map[string]*models.TransactionTag, len(existTags)) + + for i := 0; i < len(existTags); i++ { + tag := existTags[i] + existsNameTagMap[tag.Name] = tag + } + + newTags := make([]*models.TransactionTag, 0, len(tags)-len(existTags)) + + for i := 0; i < len(tags); i++ { + tag := tags[i] + existsTag, exists := existsNameTagMap[tag.Name] + + if exists { + tag.FillFromOtherTag(existsTag) + continue + } + + newTags = append(newTags, tag) + } + + tagUuids := s.GenerateUuids(uuid.UUID_TYPE_TAG_INDEX, uint16(len(newTags))) + + if len(tagUuids) < len(newTags) { + return errs.ErrSystemIsBusy + } + + for i := 0; i < len(newTags); i++ { + tag := newTags[i] + tag.TagId = tagUuids[i] + tag.Deleted = false + tag.CreatedUnixTime = time.Now().Unix() + tag.UpdatedUnixTime = time.Now().Unix() + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + for i := 0; i < len(newTags); i++ { + tag := newTags[i] + _, err := sess.Insert(tag) + + if err != nil { + return err + } + } + + return nil + }) +} + // ModifyTag saves an existed transaction tag model to database func (s *TransactionTagService) ModifyTag(c core.Context, tag *models.TransactionTag) error { if tag.Uid <= 0 { diff --git a/src/lib/services.ts b/src/lib/services.ts index 09e15dff..41c15888 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -80,6 +80,7 @@ import type { } from '@/models/transaction_picture_info.ts'; import type { TransactionTagCreateRequest, + TransactionTagCreateBatchRequest, TransactionTagModifyRequest, TransactionTagHideRequest, TransactionTagMoveRequest, @@ -523,6 +524,9 @@ export default { addTransactionTag: (req: TransactionTagCreateRequest): ApiResponsePromise => { return axios.post>('v1/transaction/tags/add.json', req); }, + addTransactionTagBatch: (req: TransactionTagCreateBatchRequest): ApiResponsePromise => { + return axios.post>('v1/transaction/tags/add_batch.json', req); + }, modifyTransactionTag: (req: TransactionTagModifyRequest): ApiResponsePromise => { return axios.post>('v1/transaction/tags/modify.json', req); }, diff --git a/src/models/transaction_tag.ts b/src/models/transaction_tag.ts index 52a8d591..e2b09ad2 100644 --- a/src/models/transaction_tag.ts +++ b/src/models/transaction_tag.ts @@ -47,6 +47,11 @@ export interface TransactionTagCreateRequest { readonly name: string; } +export interface TransactionTagCreateBatchRequest { + readonly tags: TransactionTagCreateRequest[]; + readonly skipExists: boolean; +} + export interface TransactionTagModifyRequest { readonly id: string; readonly name: string; diff --git a/src/stores/transactionTag.ts b/src/stores/transactionTag.ts index 29216fcf..c10698a3 100644 --- a/src/stores/transactionTag.ts +++ b/src/stores/transactionTag.ts @@ -4,6 +4,7 @@ import { defineStore } from 'pinia'; import type { BeforeResolveFunction } from '@/core/base.ts'; import { + type TransactionTagCreateBatchRequest, type TransactionTagInfoResponse, type TransactionTagNewDisplayOrderRequest, TransactionTag @@ -189,6 +190,37 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => { }); } + function addTags(req: TransactionTagCreateBatchRequest): Promise { + return new Promise((resolve, reject) => { + services.addTransactionTagBatch(req).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to add tag' }); + return; + } + + if (!transactionTagListStateInvalid.value) { + updateTransactionTagListInvalidState(true); + } + + const transactionTags = TransactionTag.ofMulti(data.result); + + resolve(transactionTags); + }).catch(error => { + logger.error('failed to add tags', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to add tag' }); + } else { + reject(error); + } + }); + }); + } + function changeTagDisplayOrder({ tagId, from, to }: { tagId: string, from: number, to: number }): Promise { return new Promise((resolve, reject) => { let tag: TransactionTag | null = null; @@ -342,6 +374,7 @@ export const useTransactionTagsStore = defineStore('transactionTags', () => { resetTransactionTags, loadAllTags, saveTag, + addTags, changeTagDisplayOrder, updateTagDisplayOrders, hideTag, diff --git a/src/views/desktop/transactions/import/ImportDialog.vue b/src/views/desktop/transactions/import/ImportDialog.vue index 1eb09022..1155b195 100644 --- a/src/views/desktop/transactions/import/ImportDialog.vue +++ b/src/views/desktop/transactions/import/ImportDialog.vue @@ -155,10 +155,10 @@ :disabled="!!editingTransaction || !allInvalidTransferCategoryNames || allInvalidTransferCategoryNames.length < 1" :title="tt('Create Nonexistent Transfer Categories')" @click="showBatchCreateInvalidItemDialog('transferCategory', allInvalidTransferCategoryNames)"> - - - - + ('snackbar'); @@ -151,6 +154,31 @@ function buildBatchCreateCategoryResponse(createdCategories: Record = {}; + const sourceTargetMap: Record = {}; + + for (const item of (invalidItems.value || [])) { + displayNameSourceItemMap[item.name] = item.value; + } + + for (const tag of createdTags) { + const sourceItem = displayNameSourceItemMap[tag.name]; + + if (!isDefined(sourceItem)) { + continue; + } + + sourceTargetMap[sourceItem] = tag.id; + } + + const response: BatchCreateDialogResponse = { + sourceTargetMap: sourceTargetMap + }; + + return response; +} + function open(options: { type: BatchCreateDialogDataType, invalidItems?: NameValue[] }): Promise { type.value = options.type; invalidItems.value = options.invalidItems; @@ -241,7 +269,38 @@ function confirm(): void { } }); } else if (type.value === 'tag') { + submitting.value = true; + const submitTags: TransactionTagCreateRequest[] = []; + + for (const item of selectedNames.value) { + const tag: TransactionTag = TransactionTag.createNewTag(item); + submitTags.push(tag.toCreateRequest()); + } + + transactionTagsStore.addTags({ + tags: submitTags, + skipExists: true + }).then(response => { + transactionTagsStore.loadAllTags({ force: false }).then(() => { + submitting.value = false; + showState.value = false; + + resolveFunc?.(buildBatchCreateTagResponse(response)); + }).catch(error => { + submitting.value = false; + + if (!error.processed) { + snackbar.value?.showError(error); + } + }); + }).catch(error => { + submitting.value = false; + + if (!error.processed) { + snackbar.value?.showError(error); + } + }); } }