add transaction template
This commit is contained in:
@@ -116,5 +116,13 @@ func updateAllDatabaseTablesStructure() error {
|
||||
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -320,6 +320,16 @@ func startWebServer(c *cli.Context) error {
|
||||
apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler))
|
||||
apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler))
|
||||
|
||||
// Transaction Templates
|
||||
apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler))
|
||||
apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler))
|
||||
apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler))
|
||||
apiV1Route.POST("/transaction/templates/modify_name.json", bindApi(api.TransactionTemplates.TemplateModifyNameHandler))
|
||||
apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler))
|
||||
apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler))
|
||||
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
|
||||
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
|
||||
|
||||
// Exchange Rates
|
||||
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type DataManagementsApi struct {
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
templates *services.TransactionTemplateService
|
||||
}
|
||||
|
||||
// Initialize a data management api singleton instance
|
||||
@@ -40,6 +41,7 @@ var (
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
templates: services.TransactionTemplates,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -84,11 +86,19 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionTemplateCount, err := a.templates.GetTotalTemplateCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
dataStatisticsResp := &models.DataStatisticsResponse{
|
||||
TotalAccountCount: totalAccountCount,
|
||||
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
||||
TotalTransactionTagCount: totalTransactionTagCount,
|
||||
TotalTransactionCount: totalTransactionCount,
|
||||
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
||||
}
|
||||
|
||||
return dataStatisticsResp, nil
|
||||
@@ -140,6 +150,13 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.templates.DeleteAllTemplates(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// TransactionTemplatesApi represents transaction template api
|
||||
type TransactionTemplatesApi struct {
|
||||
templates *services.TransactionTemplateService
|
||||
}
|
||||
|
||||
// Initialize a transaction template api singleton instance
|
||||
var (
|
||||
TransactionTemplates = &TransactionTemplatesApi{
|
||||
templates: services.TransactionTemplates,
|
||||
}
|
||||
)
|
||||
|
||||
// TemplateListHandler returns transaction template list of current user
|
||||
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateListReq models.TransactionTemplateListRequest
|
||||
err := c.ShouldBindQuery(&templateListReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
templateResps := make(models.TransactionTemplateInfoResponseSlice, len(templates))
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
|
||||
for i := 0; i < len(templates); i++ {
|
||||
templateResps[i] = templates[i].ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
}
|
||||
|
||||
sort.Sort(templateResps)
|
||||
|
||||
return templateResps, nil
|
||||
}
|
||||
|
||||
// TemplateGetHandler returns one specific transaction template of current user
|
||||
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateGetReq models.TransactionTemplateGetRequest
|
||||
err := c.ShouldBindQuery(&templateGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateCreateHandler saves a new transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateCreateReq models.TransactionTemplateCreateRequest
|
||||
err := c.ShouldBindJSON(&templateCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
||||
|
||||
if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
||||
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
||||
|
||||
if found {
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||
templateId, err := utils.StringToInt64(remark)
|
||||
|
||||
if err == nil {
|
||||
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.templates.CreateTemplate(c, template)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
|
||||
|
||||
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateModifyNameHandler updates the name of an existed transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateModifyNameHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateModifyReq models.TransactionTemplateModifyNameRequest
|
||||
err := c.ShouldBindJSON(&templateModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if templateModifyReq.Name == template.Name {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
newTemplate := &models.TransactionTemplate{
|
||||
TemplateId: template.TemplateId,
|
||||
Uid: template.Uid,
|
||||
Name: templateModifyReq.Name,
|
||||
}
|
||||
|
||||
err = a.templates.ModifyTemplateName(c, newTemplate)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyNameHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
template.Name = newTemplate.Name
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateModifyReq models.TransactionTemplateModifyRequest
|
||||
err := c.ShouldBindJSON(&templateModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
newTemplate := &models.TransactionTemplate{
|
||||
TemplateId: template.TemplateId,
|
||||
Uid: uid,
|
||||
Type: templateModifyReq.Type,
|
||||
CategoryId: templateModifyReq.CategoryId,
|
||||
AccountId: templateModifyReq.SourceAccountId,
|
||||
TagIds: strings.Join(templateModifyReq.TagIds, ","),
|
||||
Amount: templateModifyReq.SourceAmount,
|
||||
RelatedAccountId: templateModifyReq.DestinationAccountId,
|
||||
RelatedAccountAmount: templateModifyReq.DestinationAmount,
|
||||
Comment: templateModifyReq.Comment,
|
||||
}
|
||||
|
||||
if newTemplate.Type == template.Type &&
|
||||
newTemplate.CategoryId == template.CategoryId &&
|
||||
newTemplate.AccountId == template.AccountId &&
|
||||
newTemplate.TagIds == template.TagIds &&
|
||||
newTemplate.Amount == template.Amount &&
|
||||
newTemplate.RelatedAccountId == template.RelatedAccountId &&
|
||||
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
||||
newTemplate.Comment == template.Comment {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
err = a.templates.ModifyTemplate(c, newTemplate)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
newTemplate.Name = template.Name
|
||||
newTemplate.DisplayOrder = template.DisplayOrder
|
||||
newTemplate.Hidden = template.Hidden
|
||||
templateResp := newTemplate.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateHideHandler hides an transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateHideReq models.TransactionTemplateHideRequest
|
||||
err := c.ShouldBindJSON(&templateHideReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateMoveReq models.TransactionTemplateMoveRequest
|
||||
err := c.ShouldBindJSON(&templateMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
|
||||
|
||||
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
||||
newDisplayOrder := templateMoveReq.NewDisplayOrders[i]
|
||||
template := &models.TransactionTemplate{
|
||||
Uid: uid,
|
||||
TemplateId: newDisplayOrder.Id,
|
||||
DisplayOrder: newDisplayOrder.DisplayOrder,
|
||||
}
|
||||
|
||||
templates[i] = template
|
||||
}
|
||||
|
||||
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.Context) (any, *errs.Error) {
|
||||
var templateDeleteReq models.TransactionTemplateDeleteRequest
|
||||
err := c.ShouldBindJSON(&templateDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
||||
return &models.TransactionTemplate{
|
||||
Uid: uid,
|
||||
TemplateType: templateCreateReq.TemplateType,
|
||||
Name: templateCreateReq.Name,
|
||||
Type: models.TRANSACTION_TYPE_EXPENSE,
|
||||
DisplayOrder: order,
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ const (
|
||||
DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT DuplicateCheckerType = 1
|
||||
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
|
||||
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
|
||||
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
NormalSubcategoryTag = 7
|
||||
NormalSubcategoryDataManagement = 8
|
||||
NormalSubcategoryMapProxy = 9
|
||||
NormalSubcategoryTemplate = 10
|
||||
)
|
||||
|
||||
// Error represents the specific error returned to user
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package errs
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Error codes related to transaction templates
|
||||
var (
|
||||
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
|
||||
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
|
||||
)
|
||||
@@ -11,4 +11,5 @@ type DataStatisticsResponse struct {
|
||||
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
|
||||
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
|
||||
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
|
||||
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// TransactionTemplateType represents transaction template type in database
|
||||
type TransactionTemplateType byte
|
||||
|
||||
// Transaction template types
|
||||
const (
|
||||
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
|
||||
)
|
||||
|
||||
// TransactionTemplate represents transaction template stored in database
|
||||
type TransactionTemplate struct {
|
||||
TemplateId int64 `xorm:"PK"`
|
||||
Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
||||
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
||||
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
||||
Type TransactionType `xorm:"NOT NULL"`
|
||||
CategoryId int64 `xorm:"NOT NULL"`
|
||||
AccountId int64 `xorm:"NOT NULL"`
|
||||
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Amount int64 `xorm:"NOT NULL"`
|
||||
RelatedAccountId int64 `xorm:"NOT NULL"`
|
||||
RelatedAccountAmount int64 `xorm:"NOT NULL"`
|
||||
Comment string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
||||
Hidden bool `xorm:"NOT NULL"`
|
||||
CreatedUnixTime int64
|
||||
UpdatedUnixTime int64
|
||||
DeletedUnixTime int64
|
||||
}
|
||||
|
||||
// TransactionTemplateListRequest represents all parameters of transaction template list request
|
||||
type TransactionTemplateListRequest struct {
|
||||
TemplateType TransactionTemplateType `form:"templateType" binding:"required,min=1,max=1"`
|
||||
}
|
||||
|
||||
// TransactionTemplateGetRequest represents all parameters of transaction template getting request
|
||||
type TransactionTemplateGetRequest struct {
|
||||
Id int64 `form:"id,string" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
|
||||
type TransactionTemplateCreateRequest struct {
|
||||
TemplateType TransactionTemplateType `json:"templateType" binding:"required,min=1,max=1"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
||||
ClientSessionId string `json:"clientSessionId"`
|
||||
}
|
||||
|
||||
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
|
||||
type TransactionTemplateModifyNameRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
||||
}
|
||||
|
||||
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
|
||||
type TransactionTemplateModifyRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
Type TransactionType `json:"type" binding:"required"`
|
||||
CategoryId int64 `json:"categoryId,string"`
|
||||
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
||||
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
|
||||
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
|
||||
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
|
||||
TagIds []string `json:"tagIds"`
|
||||
Comment string `json:"comment" binding:"max=255"`
|
||||
}
|
||||
|
||||
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
|
||||
type TransactionTemplateHideRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// TransactionTemplateMoveRequest represents all parameters of transaction template moving request
|
||||
type TransactionTemplateMoveRequest struct {
|
||||
NewDisplayOrders []*TransactionTemplateNewDisplayOrderRequest `json:"newDisplayOrders"`
|
||||
}
|
||||
|
||||
// TransactionTemplateNewDisplayOrderRequest represents a data pair of id and display order
|
||||
type TransactionTemplateNewDisplayOrderRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
DisplayOrder int32 `json:"displayOrder"`
|
||||
}
|
||||
|
||||
// TransactionTemplateDeleteRequest represents all parameters of transaction template deleting request
|
||||
type TransactionTemplateDeleteRequest struct {
|
||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type TransactionTemplateInfoResponse struct {
|
||||
*TransactionInfoResponse
|
||||
TemplateType TransactionTemplateType `json:"templateType"`
|
||||
Name string `json:"name"`
|
||||
DisplayOrder int32 `json:"displayOrder"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
// ToTransactionInfoResponse returns a view-object according to database model
|
||||
func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
|
||||
tagIds := make([]string, 0, 0)
|
||||
|
||||
if t.TagIds != "" {
|
||||
tagIds = strings.Split(t.TagIds, ",")
|
||||
}
|
||||
|
||||
return &TransactionInfoResponse{
|
||||
Id: t.TemplateId,
|
||||
TimeSequenceId: utils.GetMinTransactionTimeFromUnixTime(t.CreatedUnixTime),
|
||||
Type: t.Type,
|
||||
CategoryId: t.CategoryId,
|
||||
Time: 0,
|
||||
UtcOffset: utcOffset,
|
||||
SourceAccountId: t.AccountId,
|
||||
DestinationAccountId: t.RelatedAccountId,
|
||||
SourceAmount: t.Amount,
|
||||
DestinationAmount: t.RelatedAccountAmount,
|
||||
HideAmount: false,
|
||||
TagIds: tagIds,
|
||||
Comment: t.Comment,
|
||||
GeoLocation: nil,
|
||||
Editable: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ToTransactionTemplateInfoResponse returns a view-object according to database model
|
||||
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(utcOffset int16) *TransactionTemplateInfoResponse {
|
||||
return &TransactionTemplateInfoResponse{
|
||||
TransactionInfoResponse: t.ToTransactionInfoResponse(utcOffset),
|
||||
TemplateType: t.TemplateType,
|
||||
Name: t.Name,
|
||||
DisplayOrder: t.DisplayOrder,
|
||||
Hidden: t.Hidden,
|
||||
}
|
||||
}
|
||||
|
||||
// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse
|
||||
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
|
||||
|
||||
// Len returns the count of items
|
||||
func (s TransactionTemplateInfoResponseSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Swap swaps two items
|
||||
func (s TransactionTemplateInfoResponseSlice) 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 TransactionTemplateInfoResponseSlice) Less(i, j int) bool {
|
||||
return s[i].DisplayOrder < s[j].DisplayOrder
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// TransactionTemplateService represents transaction template service
|
||||
type TransactionTemplateService struct {
|
||||
ServiceUsingDB
|
||||
ServiceUsingUuid
|
||||
}
|
||||
|
||||
// Initialize a transaction template service singleton instance
|
||||
var (
|
||||
TransactionTemplates = &TransactionTemplateService{
|
||||
ServiceUsingDB: ServiceUsingDB{
|
||||
container: datastore.Container,
|
||||
},
|
||||
ServiceUsingUuid: ServiceUsingUuid{
|
||||
container: uuid.Container,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// GetTotalTemplateCountByUid returns total template count of user
|
||||
func (s *TransactionTemplateService) GetTotalTemplateCountByUid(c *core.Context, uid int64) (int64, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Count(&models.TransactionTemplate{})
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetAllTemplatesByUid returns all transaction template models of user
|
||||
func (s *TransactionTemplateService) GetAllTemplatesByUid(c *core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
var templates []*models.TransactionTemplate
|
||||
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).Find(&templates)
|
||||
|
||||
return templates, err
|
||||
}
|
||||
|
||||
// GetTemplateByTemplateId returns a transaction template model according to transaction template id
|
||||
func (s *TransactionTemplateService) GetTemplateByTemplateId(c *core.Context, uid int64, templateId int64) (*models.TransactionTemplate, error) {
|
||||
if uid <= 0 {
|
||||
return nil, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
if templateId <= 0 {
|
||||
return nil, errs.ErrTransactionTemplateIdInvalid
|
||||
}
|
||||
|
||||
template := &models.TransactionTemplate{}
|
||||
has, err := s.UserDataDB(uid).NewSession(c).ID(templateId).Where("uid=? AND deleted=?", uid, false).Get(template)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetMaxDisplayOrder returns the max display order
|
||||
func (s *TransactionTemplateService) GetMaxDisplayOrder(c *core.Context, uid int64, templateType models.TransactionTemplateType) (int32, error) {
|
||||
if uid <= 0 {
|
||||
return 0, errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
template := &models.TransactionTemplate{}
|
||||
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).OrderBy("display_order desc").Limit(1).Get(template)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if has {
|
||||
return template.DisplayOrder, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTemplate saves a new transaction template model to database
|
||||
func (s *TransactionTemplateService) CreateTemplate(c *core.Context, template *models.TransactionTemplate) error {
|
||||
if template.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
template.TemplateId = s.GenerateUuid(uuid.UUID_TYPE_TEMPLATE)
|
||||
|
||||
if template.TemplateId < 1 {
|
||||
return errs.ErrSystemIsBusy
|
||||
}
|
||||
|
||||
template.Deleted = false
|
||||
template.CreatedUnixTime = time.Now().Unix()
|
||||
template.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
_, err := sess.Insert(template)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ModifyTemplate saves an existed transaction template model to database
|
||||
func (s *TransactionTemplateService) ModifyTemplate(c *core.Context, template *models.TransactionTemplate) error {
|
||||
if template.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
template.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.ID(template.TemplateId).Cols("type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ModifyTemplateName updates the name of an existed transaction template model to database
|
||||
func (s *TransactionTemplateService) ModifyTemplateName(c *core.Context, template *models.TransactionTemplate) error {
|
||||
if template.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
template.UpdatedUnixTime = time.Now().Unix()
|
||||
|
||||
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// HideTemplate updates hidden field of given transaction templates
|
||||
func (s *TransactionTemplateService) HideTemplate(c *core.Context, uid int64, ids []int64, hidden bool) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.TransactionTemplate{
|
||||
Hidden: hidden,
|
||||
UpdatedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("template_id", ids).Update(updateModel)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ModifyTemplateDisplayOrders updates display order of given transaction templates
|
||||
func (s *TransactionTemplateService) ModifyTemplateDisplayOrders(c *core.Context, uid int64, templates []*models.TransactionTemplate) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
for i := 0; i < len(templates); i++ {
|
||||
templates[i].UpdatedUnixTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
for i := 0; i < len(templates); i++ {
|
||||
template := templates[i]
|
||||
updatedRows, err := sess.ID(template.TemplateId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(template)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTemplate deletes an existed transaction template from database
|
||||
func (s *TransactionTemplateService) DeleteTemplate(c *core.Context, uid int64, templateId int64) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.TransactionTemplate{
|
||||
Deleted: true,
|
||||
DeletedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
deletedRows, err := sess.ID(templateId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if deletedRows < 1 {
|
||||
return errs.ErrTransactionTemplateNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllTemplates deletes all existed transaction templates from database
|
||||
func (s *TransactionTemplateService) DeleteAllTemplates(c *core.Context, uid int64) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.TransactionTemplate{
|
||||
Deleted: true,
|
||||
DeletedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
_, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -178,6 +178,14 @@ func GetTimezoneOffsetMinutes(timezone *time.Location) int16 {
|
||||
return tzMinuteOffset
|
||||
}
|
||||
|
||||
// GetServerTimezoneOffsetMinutes returns offset minutes of current server timezone
|
||||
func GetServerTimezoneOffsetMinutes() int16 {
|
||||
_, tzOffset := time.Now().Zone()
|
||||
tzMinuteOffset := int16(tzOffset / 60)
|
||||
|
||||
return tzMinuteOffset
|
||||
}
|
||||
|
||||
// FormatTimezoneOffset returns "+/-HH:MM" format of timezone
|
||||
func FormatTimezoneOffset(timezone *time.Location) string {
|
||||
tzMinutesOffset := GetTimezoneOffsetMinutes(timezone)
|
||||
|
||||
@@ -12,4 +12,5 @@ const (
|
||||
UUID_TYPE_CATEGORY UuidType = 4
|
||||
UUID_TYPE_TAG UuidType = 5
|
||||
UUID_TYPE_TAG_INDEX UuidType = 6
|
||||
UUID_TYPE_TEMPLATE UuidType = 7
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const allTemplateTypes = {
|
||||
Normal: 1
|
||||
};
|
||||
|
||||
export default {
|
||||
allTemplateTypes: allTemplateTypes,
|
||||
}
|
||||
@@ -37,6 +37,10 @@ export function categoryTypeToTransactionType(categoryType) {
|
||||
}
|
||||
|
||||
export function getTransactionPrimaryCategoryName(categoryId, allCategories) {
|
||||
if (!allCategories) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let i = 0; i < allCategories.length; i++) {
|
||||
for (let j = 0; j < allCategories[i].subCategories.length; j++) {
|
||||
const subCategory = allCategories[i].subCategories[j];
|
||||
@@ -50,6 +54,10 @@ export function getTransactionPrimaryCategoryName(categoryId, allCategories) {
|
||||
}
|
||||
|
||||
export function getTransactionSecondaryCategoryName(categoryId, allCategories) {
|
||||
if (!allCategories) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let i = 0; i < allCategories.length; i++) {
|
||||
for (let j = 0; j < allCategories[i].subCategories.length; j++) {
|
||||
const subCategory = allCategories[i].subCategories[j];
|
||||
|
||||
@@ -517,6 +517,54 @@ export default {
|
||||
id
|
||||
});
|
||||
},
|
||||
getAllTransactionTemplates: ({ templateType }) => {
|
||||
return axios.get('v1/transaction/templates/list.json?templateType=' + templateType);
|
||||
},
|
||||
getTransactionTemplate: ({ id }) => {
|
||||
return axios.get('v1/transaction/templates/get.json?id=' + id);
|
||||
},
|
||||
addTransactionTemplate: ({ templateType, name, clientSessionId }) => {
|
||||
return axios.post('v1/transaction/templates/add.json', {
|
||||
templateType,
|
||||
name,
|
||||
clientSessionId
|
||||
});
|
||||
},
|
||||
modifyTransactionNameTemplate: ({ id, name }) => {
|
||||
return axios.post('v1/transaction/templates/modify_name.json', {
|
||||
id,
|
||||
name
|
||||
});
|
||||
},
|
||||
modifyTransactionTemplate: ({ id, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, tagIds, comment }) => {
|
||||
return axios.post('v1/transaction/templates/modify.json', {
|
||||
id,
|
||||
type,
|
||||
categoryId,
|
||||
sourceAccountId,
|
||||
destinationAccountId,
|
||||
sourceAmount,
|
||||
destinationAmount,
|
||||
tagIds,
|
||||
comment
|
||||
});
|
||||
},
|
||||
hideTransactionTemplate: ({ id, hidden }) => {
|
||||
return axios.post('v1/transaction/templates/hide.json', {
|
||||
id,
|
||||
hidden
|
||||
});
|
||||
},
|
||||
moveTransactionTemplate: ({ newDisplayOrders }) => {
|
||||
return axios.post('v1/transaction/templates/move.json', {
|
||||
newDisplayOrders,
|
||||
});
|
||||
},
|
||||
deleteTransactionTemplate: ({ id }) => {
|
||||
return axios.post('v1/transaction/templates/delete.json', {
|
||||
id
|
||||
});
|
||||
},
|
||||
getLatestExchangeRates: ({ ignoreError }) => {
|
||||
return axios.get('v1/exchange_rates/latest.json', {
|
||||
ignoreError: !!ignoreError
|
||||
|
||||
@@ -788,6 +788,7 @@ export default {
|
||||
'Show': 'Show',
|
||||
'Hide': 'Hide',
|
||||
'Version': 'Version',
|
||||
'Modify Name': 'Modify Name',
|
||||
'Edit': 'Edit',
|
||||
'Remove': 'Remove',
|
||||
'Delete': 'Delete',
|
||||
@@ -1020,6 +1021,7 @@ export default {
|
||||
'Transactions': 'Transactions',
|
||||
'Add Transaction': 'Add Transaction',
|
||||
'Edit Transaction': 'Edit Transaction',
|
||||
'Edit Transaction Template': 'Edit Transaction Template',
|
||||
'Modify Balance': 'Modify Balance',
|
||||
'Expense Amount': 'Expense Amount',
|
||||
'Income Amount': 'Income Amount',
|
||||
@@ -1268,6 +1270,23 @@ export default {
|
||||
'Unable to delete this tag': 'Unable to delete this tag',
|
||||
'Show Hidden Transaction Tags': 'Show Hidden Transaction Tags',
|
||||
'Hide Hidden Transaction Tags': 'Hide Hidden Transaction Tags',
|
||||
'Transaction Templates': 'Transaction Templates',
|
||||
'Template Name': 'Template Name',
|
||||
'No available template': 'No available template',
|
||||
'Unable to retrieve template list': 'Unable to retrieve template list',
|
||||
'Template list is up to date': 'Template list is up to date',
|
||||
'Template list has been updated': 'Template list has been updated',
|
||||
'Unable to add template': 'Unable to add template',
|
||||
'Unable to save template': 'Unable to save template',
|
||||
'Unable to move template': 'Unable to move template',
|
||||
'Unable to retrieve template': 'Unable to retrieve template',
|
||||
'Unable to hide this template': 'Unable to hide this template',
|
||||
'Unable to unhide this template': 'Unable to unhide this template',
|
||||
'Are you sure you want to delete this template?': 'Are you sure you want to delete this template?',
|
||||
'Unable to delete this template': 'Unable to delete this template',
|
||||
'You have saved this template': 'You have saved this template',
|
||||
'Show Hidden Templates': 'Show Hidden Transaction Templates',
|
||||
'Hide Hidden Templates': 'Hide Hidden Transaction Templates',
|
||||
'Are you sure you want to logout from this session?': 'Are you sure you want to logout from this session?',
|
||||
'Unable to logout from this session': 'Unable to logout from this session',
|
||||
'Are you sure you want to logout all other sessions?': 'Are you sure you want to logout all other sessions?',
|
||||
|
||||
@@ -788,6 +788,7 @@ export default {
|
||||
'Show': '显示',
|
||||
'Hide': '隐藏',
|
||||
'Version': '版本',
|
||||
'Modify Name': '修改名称',
|
||||
'Edit': '编辑',
|
||||
'Remove': '移除',
|
||||
'Delete': '删除',
|
||||
@@ -1020,6 +1021,7 @@ export default {
|
||||
'Transactions': '交易',
|
||||
'Add Transaction': '添加交易',
|
||||
'Edit Transaction': '编辑交易',
|
||||
'Edit Transaction Template': '编辑交易模板',
|
||||
'Modify Balance': '修改余额',
|
||||
'Expense Amount': '支出金额',
|
||||
'Income Amount': '收入金额',
|
||||
@@ -1268,6 +1270,23 @@ export default {
|
||||
'Unable to delete this tag': '无法删除该标签',
|
||||
'Show Hidden Transaction Tags': '显示隐藏的交易标签',
|
||||
'Hide Hidden Transaction Tags': '不显示隐藏的交易标签',
|
||||
'Transaction Templates': '交易模板',
|
||||
'Template Name': '模板名称',
|
||||
'No available template': '没有可用的模板',
|
||||
'Unable to retrieve template list': '无法获取模板列表',
|
||||
'Template list is up to date': '模板列表已是最新',
|
||||
'Template list has been updated': '模板列表已更新',
|
||||
'Unable to add template': '无法添加模板',
|
||||
'Unable to save template': '无法保存模板',
|
||||
'Unable to move template': '无法移动模板',
|
||||
'Unable to retrieve template': '无法获取模板',
|
||||
'Unable to hide this template': '无法隐藏该模板',
|
||||
'Unable to unhide this template': '无法取消隐藏该模板',
|
||||
'Are you sure you want to delete this template?': '您确定要删除该模板?',
|
||||
'Unable to delete this template': '无法删除该模板',
|
||||
'You have saved this template': '您已经保存该模板',
|
||||
'Show Hidden Templates': '显示隐藏的模板',
|
||||
'Hide Hidden Templates': '不显示隐藏的模板',
|
||||
'Are you sure you want to logout from this session?': '您确定要退出该会话?',
|
||||
'Unable to logout from this session': '无法退出该会话',
|
||||
'Are you sure you want to logout all other sessions?': '您确定要退出其他所有会话?',
|
||||
|
||||
@@ -22,6 +22,8 @@ import TransactionCategoryListPage from '@/views/desktop/categories/ListPage.vue
|
||||
|
||||
import TransactionTagListPage from '@/views/desktop/tags/ListPage.vue';
|
||||
|
||||
import TransactionTemplateListPage from '@/views/desktop/templates/ListPage.vue';
|
||||
|
||||
import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue';
|
||||
import AppSettingsPage from '@/views/desktop/app/AppSettingsPage.vue';
|
||||
|
||||
@@ -136,6 +138,11 @@ const router = createRouter({
|
||||
component: TransactionTagListPage,
|
||||
beforeEnter: checkLogin
|
||||
},
|
||||
{
|
||||
path: '/template/list',
|
||||
component: TransactionTemplateListPage,
|
||||
beforeEnter: checkLogin
|
||||
},
|
||||
{
|
||||
path: '/exchange_rates',
|
||||
component: ExchangeRatesPage,
|
||||
|
||||
@@ -38,6 +38,8 @@ import CategoryPresetPage from '@/views/mobile/categories/PresetPage.vue';
|
||||
|
||||
import TagListPage from '@/views/mobile/tags/ListPage.vue';
|
||||
|
||||
import TemplateListPage from '@/views/mobile/templates/ListPage.vue';
|
||||
|
||||
function asyncResolve(component) {
|
||||
return function({ resolve }) {
|
||||
return resolve({
|
||||
@@ -290,6 +292,11 @@ const routes = [
|
||||
async: asyncResolve(TagListPage),
|
||||
beforeEnter: [checkLogin]
|
||||
},
|
||||
{
|
||||
path: '/template/list',
|
||||
async: asyncResolve(TemplateListPage),
|
||||
beforeEnter: [checkLogin]
|
||||
},
|
||||
{
|
||||
path: '(.*)',
|
||||
redirect: '/'
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useUserStore } from './user.js';
|
||||
import { useAccountsStore } from './account.js';
|
||||
import { useTransactionCategoriesStore } from './transactionCategory.js';
|
||||
import { useTransactionTagsStore } from './transactionTag.js';
|
||||
import { useTransactionTemplatesStore } from './transactionTemplate.js';
|
||||
import { useTransactionsStore } from './transaction.js';
|
||||
import { useOverviewStore } from './overview.js';
|
||||
import { useStatisticsStore } from './statistics.js';
|
||||
@@ -38,6 +39,9 @@ export const useRootStore = defineStore('root', {
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
transactionCategoriesStore.resetTransactionCategories();
|
||||
|
||||
const transactionTemplatesStore = useTransactionTemplatesStore();
|
||||
transactionTemplatesStore.resetTransactionTemplates();
|
||||
|
||||
const accountsStore = useAccountsStore();
|
||||
accountsStore.resetAccounts();
|
||||
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import transactionConstants from '@/consts/transaction.js';
|
||||
import { isDefined, isObject, isArray, isEquals } from '@/lib/common.js';
|
||||
import services from '@/lib/services.js';
|
||||
import logger from '@/lib/logger.js';
|
||||
|
||||
function loadTransactionTemplateList(state, templateType, templates) {
|
||||
state.allTransactionTemplates[templateType] = templates;
|
||||
state.allTransactionTemplatesMap[templateType] = {};
|
||||
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
const template = templates[i];
|
||||
state.allTransactionTemplatesMap[templateType][template.id] = template;
|
||||
}
|
||||
}
|
||||
|
||||
function addTemplateToTransactionTemplateList(state, templateType, template) {
|
||||
if (isArray(state.allTransactionTemplates[templateType])) {
|
||||
state.allTransactionTemplates[templateType].push(template);
|
||||
}
|
||||
|
||||
if (isObject(state.allTransactionTemplatesMap[templateType])) {
|
||||
state.allTransactionTemplatesMap[templateType][template.id] = template;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTemplateInTransactionTemplateList(state, templateType, template) {
|
||||
if (isArray(state.allTransactionTemplates[templateType])) {
|
||||
for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) {
|
||||
if (state.allTransactionTemplates[templateType][i].id === template.id) {
|
||||
state.allTransactionTemplates[templateType].splice(i, 1, template);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(state.allTransactionTemplatesMap[templateType])) {
|
||||
state.allTransactionTemplatesMap[templateType][template.id] = template;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTemplateDisplayOrderInTransactionTemplateList(state, templateType, { from, to }) {
|
||||
if (isArray(state.allTransactionTemplates[templateType])) {
|
||||
state.allTransactionTemplates[templateType].splice(to, 0, state.allTransactionTemplates[templateType].splice(from, 1)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTemplateVisibilityInTransactionTemplateList(state, templateType, { template, hidden }) {
|
||||
if (isObject(state.allTransactionTemplatesMap[templateType])) {
|
||||
if (state.allTransactionTemplatesMap[templateType][template.id]) {
|
||||
state.allTransactionTemplatesMap[templateType][template.id].hidden = hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeTemplateFromTransactionTemplateList(state, templateType, template) {
|
||||
if (isArray(state.allTransactionTemplates[templateType])) {
|
||||
for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) {
|
||||
if (state.allTransactionTemplates[templateType][i].id === template.id) {
|
||||
state.allTransactionTemplates[templateType].splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(state.allTransactionTemplatesMap[templateType])) {
|
||||
if (state.allTransactionTemplatesMap[templateType][template.id]) {
|
||||
delete state.allTransactionTemplatesMap[templateType][template.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useTransactionTemplatesStore = defineStore('transactionTemplates', {
|
||||
state: () => ({
|
||||
allTransactionTemplates: {},
|
||||
allTransactionTemplatesMap: {},
|
||||
transactionTemplateListStatesInvalid: {},
|
||||
}),
|
||||
getters: {
|
||||
allVisibleTemplates(state) {
|
||||
const allVisibleTemplates = {};
|
||||
|
||||
for (const templateType in state.allTransactionTemplates) {
|
||||
if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const visibleTemplates = [];
|
||||
|
||||
for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) {
|
||||
const template = state.allTransactionTemplates[templateType][i];
|
||||
|
||||
if (!template.hidden) {
|
||||
visibleTemplates.push(template);
|
||||
}
|
||||
}
|
||||
|
||||
allVisibleTemplates[templateType] = visibleTemplates;
|
||||
}
|
||||
|
||||
return allVisibleTemplates;
|
||||
},
|
||||
allAvailableTemplatesCount(state) {
|
||||
const allAvailableTemplateCounts = {};
|
||||
|
||||
for (const templateType in state.allTransactionTemplates) {
|
||||
if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allAvailableTemplateCounts[templateType] = state.allTransactionTemplates[templateType].length;
|
||||
}
|
||||
|
||||
return allAvailableTemplateCounts;
|
||||
},
|
||||
allVisibleTemplatesCount(state) {
|
||||
const allVisibleTemplateCounts = {};
|
||||
|
||||
for (const templateType in state.allVisibleTemplates) {
|
||||
if (!Object.prototype.hasOwnProperty.call(state.allVisibleTemplates, templateType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allVisibleTemplateCounts[templateType] = state.allVisibleTemplates[templateType].length;
|
||||
}
|
||||
|
||||
return allVisibleTemplateCounts;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
generateNewTransactionTemplateModel(templateType) {
|
||||
return {
|
||||
id: '',
|
||||
templateType: templateType,
|
||||
name: ''
|
||||
};
|
||||
},
|
||||
updateTransactionTemplateListInvalidState(templateType, invalidState) {
|
||||
this.transactionTemplateListStatesInvalid[templateType] = invalidState;
|
||||
},
|
||||
resetTransactionTemplates() {
|
||||
this.allTransactionTemplates = {};
|
||||
this.allTransactionTemplatesMap = {};
|
||||
this.transactionTemplateListStatesInvalid = {};
|
||||
},
|
||||
loadAllTemplates({ templateType, force }) {
|
||||
const self = this;
|
||||
|
||||
if (!force && isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) {
|
||||
return new Promise((resolve) => {
|
||||
resolve(self.allTransactionTemplates[templateType] || []);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getAllTransactionTemplates({ templateType }).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to retrieve template list' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) {
|
||||
self.updateTransactionTemplateListInvalidState(templateType, false);
|
||||
}
|
||||
|
||||
if (force && data.result && isEquals(self.allTransactionTemplates[templateType], data.result)) {
|
||||
reject({ message: 'Template list is up to date' });
|
||||
return;
|
||||
}
|
||||
|
||||
loadTransactionTemplateList(self, templateType, data.result);
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
if (force) {
|
||||
logger.error('failed to force load template list', error);
|
||||
} else {
|
||||
logger.error('failed to load template 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 template list' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getTemplate({ templateId }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.getTransactionTemplate({
|
||||
id: templateId
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to retrieve template' });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to load template info', 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 template' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
saveTemplateName({ template }) {
|
||||
const self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let promise = null;
|
||||
|
||||
if (!template.id) {
|
||||
promise = services.addTransactionTemplate(template);
|
||||
} else {
|
||||
promise = services.modifyTransactionNameTemplate(template);
|
||||
}
|
||||
|
||||
promise.then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
if (!template.id) {
|
||||
reject({ message: 'Unable to add template' });
|
||||
} else {
|
||||
reject({ message: 'Unable to save template' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!template.id) {
|
||||
addTemplateToTransactionTemplateList(self, template.templateType, data.result);
|
||||
} else {
|
||||
updateTemplateInTransactionTemplateList(self, template.templateType, data.result);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to save template', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
if (!template.id) {
|
||||
reject({ message: 'Unable to add template' });
|
||||
} else {
|
||||
reject({ message: 'Unable to save template' });
|
||||
}
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
modifyTemplateContent({ template }) {
|
||||
const self = this;
|
||||
|
||||
const submitTemplate = {
|
||||
id: template.id,
|
||||
type: template.type,
|
||||
sourceAccountId: template.sourceAccountId,
|
||||
sourceAmount: template.sourceAmount,
|
||||
destinationAccountId: '0',
|
||||
destinationAmount: 0,
|
||||
tagIds: template.tagIds,
|
||||
comment: template.comment
|
||||
};
|
||||
|
||||
if (template.type === transactionConstants.allTransactionTypes.Expense) {
|
||||
submitTemplate.categoryId = template.expenseCategory;
|
||||
} else if (template.type === transactionConstants.allTransactionTypes.Income) {
|
||||
submitTemplate.categoryId = template.incomeCategory;
|
||||
} else if (template.type === transactionConstants.allTransactionTypes.Transfer) {
|
||||
submitTemplate.categoryId = template.transferCategory;
|
||||
submitTemplate.destinationAccountId = template.destinationAccountId;
|
||||
submitTemplate.destinationAmount = template.destinationAmount;
|
||||
} else {
|
||||
return Promise.reject('An error occurred');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.modifyTransactionTemplate(submitTemplate).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to save template' });
|
||||
return;
|
||||
}
|
||||
|
||||
updateTemplateInTransactionTemplateList(self, template.templateType, data.result);
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to save template', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
reject({ message: 'Unable to save template' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
changeTemplateDisplayOrder({ templateType, templateId, from, to }) {
|
||||
const self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let template = null;
|
||||
|
||||
if (!isArray(self.allTransactionTemplates[templateType])) {
|
||||
reject({ message: 'Unable to move template' });
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) {
|
||||
if (self.allTransactionTemplates[templateType][i].id === templateId) {
|
||||
template = self.allTransactionTemplates[templateType][i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!template || !self.allTransactionTemplates[templateType][to]) {
|
||||
reject({ message: 'Unable to move template' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) {
|
||||
self.updateTransactionTemplateListInvalidState(templateType, true);
|
||||
}
|
||||
|
||||
updateTemplateDisplayOrderInTransactionTemplateList(self, templateType, {
|
||||
template: template,
|
||||
from: from,
|
||||
to: to
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
updateTemplateDisplayOrders({ templateType }) {
|
||||
const self = this;
|
||||
const newDisplayOrders = [];
|
||||
|
||||
if (isArray(self.allTransactionTemplates[templateType])) {
|
||||
for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) {
|
||||
newDisplayOrders.push({
|
||||
id: self.allTransactionTemplates[templateType][i].id,
|
||||
displayOrder: i + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.moveTransactionTemplate({
|
||||
newDisplayOrders: newDisplayOrders
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to move template' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) {
|
||||
self.updateTransactionTemplateListInvalidState(templateType, false);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to save templates 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 template' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
hideTemplate({ template, hidden }) {
|
||||
const self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.hideTransactionTemplate({
|
||||
id: template.id,
|
||||
hidden: hidden
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
if (hidden) {
|
||||
reject({ message: 'Unable to hide this template' });
|
||||
} else {
|
||||
reject({ message: 'Unable to unhide this template' });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTemplateVisibilityInTransactionTemplateList(self, template.templateType, {
|
||||
template: template,
|
||||
hidden: hidden
|
||||
});
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to change template visibility', error);
|
||||
|
||||
if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else if (!error.processed) {
|
||||
if (hidden) {
|
||||
reject({ message: 'Unable to hide this template' });
|
||||
} else {
|
||||
reject({ message: 'Unable to unhide this template' });
|
||||
}
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteTemplate({ template, beforeResolve }) {
|
||||
const self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
services.deleteTransactionTemplate({
|
||||
id: template.id
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to delete this template' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (beforeResolve) {
|
||||
beforeResolve(() => {
|
||||
removeTemplateFromTransactionTemplateList(self, template.templateType, template);
|
||||
});
|
||||
} else {
|
||||
removeTemplateFromTransactionTemplateList(self, template.templateType, template);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to delete template', 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 template' });
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -61,6 +61,12 @@
|
||||
<span class="nav-item-title">{{ $t('Transaction Tags') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-link">
|
||||
<router-link to="/template/list">
|
||||
<v-icon class="nav-item-icon" :icon="icons.templates"/>
|
||||
<span class="nav-item-title">{{ $t('Transaction Templates') }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<span class="title-text">{{ $t('Miscellaneous') }}</span>
|
||||
@@ -196,6 +202,7 @@ import {
|
||||
mdiCreditCardOutline,
|
||||
mdiViewDashboardOutline,
|
||||
mdiTagOutline,
|
||||
mdiClipboardTextOutline,
|
||||
mdiChartPieOutline,
|
||||
mdiSwapHorizontal,
|
||||
mdiCogOutline,
|
||||
@@ -225,6 +232,7 @@ export default {
|
||||
accounts: mdiCreditCardOutline,
|
||||
categories: mdiViewDashboardOutline,
|
||||
tags: mdiTagOutline,
|
||||
templates: mdiClipboardTextOutline,
|
||||
statistics: mdiChartPieOutline,
|
||||
exchangeRates: mdiSwapHorizontal,
|
||||
settings: mdiCogOutline,
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
<template>
|
||||
<v-row class="match-height">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<span>{{ $t('Transaction Templates') }}</span>
|
||||
<v-btn class="ml-3" color="default" variant="outlined"
|
||||
:disabled="loading || updating || hasEditingTemplateName" @click="add">{{ $t('Add') }}</v-btn>
|
||||
<v-btn class="ml-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating || hasEditingTemplateName" @click="saveSortResult"
|
||||
v-if="displayOrderModified">{{ $t('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ml-2" :icon="true" :disabled="loading || updating || hasEditingTemplateName"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="icons.refresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ $t('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:disabled="loading || updating || hasEditingTemplateName" :icon="true">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="icons.show"
|
||||
:title="$t('Show Hidden Templates')"
|
||||
v-if="!showHidden" @click="showHidden = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.hide"
|
||||
:title="$t('Hide Hidden Templates')"
|
||||
v-if="showHidden" @click="showHidden = false"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-table class="transaction-templates-table table-striped" :hover="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ $t('Template Name') }}</span>
|
||||
<v-spacer/>
|
||||
<span>{{ $t('Operation') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody v-if="loading && noAvailableTemplate && !newTemplate">
|
||||
<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 && noAvailableTemplate && !newTemplate">
|
||||
<tr>
|
||||
<td>{{ $t('No available template') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
:class="{ 'has-bottom-border': newTemplate }"
|
||||
:disabled="noAvailableTemplate"
|
||||
v-model="templates"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="transaction-templates-table-row text-sm" v-if="showHidden || !element.hidden">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="editingTemplateName.id !== element.id">
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="icons.hide"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="icons.text"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="icons.text" v-else-if="!element.hidden"/>
|
||||
<span class="transaction-template-name">{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-text-field class="w-100 mr-2" type="text"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating"
|
||||
:placeholder="$t('Template Name')"
|
||||
v-model="editingTemplateName.name"
|
||||
v-else-if="editingTemplateName.id === element.id"
|
||||
@keyup.enter="saveName(editingTemplateName)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="icons.hide"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="icons.text"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="icons.text" v-else-if="!element.hidden"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2 ml-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="element.hidden ? icons.show : icons.hide"
|
||||
:loading="templateHiding[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTemplateName.id !== element.id"
|
||||
@click="hide(element, !element.hidden)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ element.hidden ? $t('Show') : $t('Hide') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="icons.edit"
|
||||
:loading="templateNameUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTemplateName.id !== element.id"
|
||||
@click="modifyName(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ $t('Modify Name') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="icons.edit"
|
||||
:loading="templateNameUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTemplateName.id !== element.id"
|
||||
@click="edit(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ $t('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="icons.remove"
|
||||
:loading="templateRemoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTemplateName.id !== element.id"
|
||||
@click="remove(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ $t('Delete') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="icons.confirm"
|
||||
:loading="templateNameUpdating[element.id]"
|
||||
:disabled="loading || updating || !isTemplateModified(element)"
|
||||
v-if="editingTemplateName.id === element.id" @click="saveName(editingTemplateName)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ $t('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="icons.cancel"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTemplateName.id === element.id" @click="cancelSaveName(editingTemplateName)">
|
||||
{{ $t('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ml-2">
|
||||
<v-icon :class="!loading && !updating && availableTemplateCount > 1 ? 'drag-handle' : 'disabled'"
|
||||
:icon="icons.drag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && availableTemplateCount > 1">{{ $t('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
|
||||
<tbody v-if="newTemplate">
|
||||
<tr class="text-sm" :class="{ 'even-row': availableTemplateCount & 1 === 1}">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field class="w-100 mr-2" type="text" color="primary"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating" :placeholder="$t('Template Name')"
|
||||
v-model="newTemplate.name" @keyup.enter="saveName(newTemplate)">
|
||||
<template #prepend>
|
||||
<v-icon size="20" start :icon="icons.text"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2" density="comfortable" variant="text"
|
||||
:prepend-icon="icons.confirm"
|
||||
:loading="templateNameUpdating[null]"
|
||||
:disabled="loading || updating || !isTemplateModified(newTemplate)"
|
||||
@click="saveName(newTemplate)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ $t('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="icons.cancel"
|
||||
:disabled="loading || updating"
|
||||
@click="cancelSaveName(newTemplate)">
|
||||
{{ $t('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ml-2">
|
||||
<v-icon class="disabled" :icon="icons.drag"/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<edit-dialog ref="editDialog" :persistent="true" />
|
||||
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js';
|
||||
|
||||
import templateConstants from '@/consts/template.js';
|
||||
import { generateRandomUUID } from '@/lib/misc.js';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiPlus,
|
||||
mdiPencilOutline,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiDeleteOutline,
|
||||
mdiDrag,
|
||||
mdiDotsVertical,
|
||||
mdiTextBoxOutline
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
templateType: templateConstants.allTemplateTypes.Normal,
|
||||
newTemplate: null,
|
||||
editingTemplateName: {},
|
||||
loading: true,
|
||||
updating: false,
|
||||
templateNameUpdating: {},
|
||||
templateHiding: {},
|
||||
templateRemoving: {},
|
||||
displayOrderModified: false,
|
||||
showHidden: false,
|
||||
icons: {
|
||||
refresh: mdiRefresh,
|
||||
add: mdiPlus,
|
||||
edit: mdiPencilOutline,
|
||||
confirm: mdiCheck,
|
||||
cancel: mdiClose,
|
||||
show: mdiEyeOutline,
|
||||
hide: mdiEyeOffOutline,
|
||||
remove: mdiDeleteOutline,
|
||||
drag: mdiDrag,
|
||||
more: mdiDotsVertical,
|
||||
text: mdiTextBoxOutline
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useTransactionTemplatesStore),
|
||||
templates() {
|
||||
return this.transactionTemplatesStore.allTransactionTemplates[this.templateType] || [];
|
||||
},
|
||||
noAvailableTemplate() {
|
||||
for (let i = 0; i < this.templates.length; i++) {
|
||||
if (this.showHidden || !this.templates[i].hidden) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
availableTemplateCount() {
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < this.templates.length; i++) {
|
||||
if (this.showHidden || !this.templates[i].hidden) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
hasEditingTemplateName() {
|
||||
return !!(this.newTemplate || (this.editingTemplateName && this.editingTemplateName.id && this.editingTemplateName.id !== ''));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const self = this;
|
||||
|
||||
self.loading = true;
|
||||
|
||||
self.transactionTemplatesStore.loadAllTemplates({
|
||||
templateType: self.templateType,
|
||||
force: false
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
if (this.hasEditingTemplateName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
self.loading = true;
|
||||
|
||||
self.transactionTemplatesStore.loadAllTemplates({
|
||||
templateType: self.templateType,
|
||||
force: true
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
self.displayOrderModified = false;
|
||||
|
||||
self.$refs.snackbar.showMessage('Template list has been updated');
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
onMove(event) {
|
||||
if (!event || !event.moved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const moveEvent = event.moved;
|
||||
|
||||
if (!moveEvent.element || !moveEvent.element.id) {
|
||||
self.$refs.snackbar.showMessage('Unable to move template');
|
||||
return;
|
||||
}
|
||||
|
||||
self.transactionTemplatesStore.changeTemplateDisplayOrder({
|
||||
templateType: self.templateType,
|
||||
templateId: moveEvent.element.id,
|
||||
from: moveEvent.oldIndex,
|
||||
to: moveEvent.newIndex
|
||||
}).then(() => {
|
||||
self.displayOrderModified = true;
|
||||
}).catch(error => {
|
||||
self.$refs.snackbar.showError(error);
|
||||
});
|
||||
},
|
||||
saveSortResult() {
|
||||
const self = this;
|
||||
|
||||
if (!self.displayOrderModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.loading = true;
|
||||
|
||||
self.transactionTemplatesStore.updateTemplateDisplayOrders({
|
||||
templateType: self.templateType
|
||||
}).then(() => {
|
||||
self.loading = false;
|
||||
self.displayOrderModified = false;
|
||||
}).catch(error => {
|
||||
self.loading = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
add() {
|
||||
this.newTemplate = {
|
||||
templateType: this.templateType,
|
||||
name: '',
|
||||
clientSessionId: generateRandomUUID()
|
||||
};
|
||||
},
|
||||
modifyName(template) {
|
||||
this.editingTemplateName.id = template.id;
|
||||
this.editingTemplateName.templateType = template.templateType;
|
||||
this.editingTemplateName.name = template.name;
|
||||
},
|
||||
saveName(template) {
|
||||
const self = this;
|
||||
|
||||
self.updating = true;
|
||||
self.templateNameUpdating[template.id || null] = true;
|
||||
|
||||
self.transactionTemplatesStore.saveTemplateName({
|
||||
template: template
|
||||
}).then(() => {
|
||||
self.updating = false;
|
||||
self.templateNameUpdating[template.id || null] = false;
|
||||
|
||||
if (template.id) {
|
||||
self.editingTemplateName = {};
|
||||
} else {
|
||||
self.newTemplate = null;
|
||||
}
|
||||
}).catch(error => {
|
||||
self.updating = false;
|
||||
self.templateNameUpdating[template.id || null] = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
cancelSaveName(template) {
|
||||
if (template.id) {
|
||||
this.editingTemplateName = {};
|
||||
} else {
|
||||
this.newTemplate = null;
|
||||
}
|
||||
},
|
||||
edit(template) {
|
||||
const self = this;
|
||||
|
||||
self.$refs.editDialog.open({
|
||||
editTemplateId: template.id,
|
||||
currentTemplate: {
|
||||
type: template.type,
|
||||
categoryId: template.categoryId,
|
||||
sourceAccountId: template.sourceAccountId,
|
||||
destinationAccountId: template.destinationAccountId,
|
||||
sourceAmount: template.sourceAmount,
|
||||
destinationAmount: template.destinationAmount,
|
||||
tagIds: template.tagIds,
|
||||
comment: template.comment
|
||||
}
|
||||
}).then(result => {
|
||||
if (result && result.message) {
|
||||
self.$refs.snackbar.showMessage(result.message);
|
||||
}
|
||||
|
||||
self.reload(false);
|
||||
}).catch(error => {
|
||||
if (error) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
isTemplateModified(template) {
|
||||
if (template.id) {
|
||||
return this.editingTemplateName && this.editingTemplateName.name !== '' && this.editingTemplateName.name !== template.name;
|
||||
} else {
|
||||
return template.name !== '';
|
||||
}
|
||||
},
|
||||
hide(template, hidden) {
|
||||
const self = this;
|
||||
|
||||
self.updating = true;
|
||||
self.templateHiding[template.id] = true;
|
||||
|
||||
self.transactionTemplatesStore.hideTemplate({
|
||||
template: template,
|
||||
hidden: hidden
|
||||
}).then(() => {
|
||||
self.updating = false;
|
||||
self.templateHiding[template.id] = false;
|
||||
}).catch(error => {
|
||||
self.updating = false;
|
||||
self.templateHiding[template.id] = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
remove(template) {
|
||||
const self = this;
|
||||
|
||||
self.$refs.confirmDialog.open('Are you sure you want to delete this template?').then(() => {
|
||||
self.updating = true;
|
||||
self.templateRemoving[template.id] = true;
|
||||
|
||||
self.transactionTemplatesStore.deleteTemplate({
|
||||
template: template
|
||||
}).then(() => {
|
||||
self.updating = false;
|
||||
self.templateRemoving[template.id] = false;
|
||||
}).catch(error => {
|
||||
self.updating = false;
|
||||
self.templateRemoving[template.id] = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transaction-templates-table tr.transaction-templates-table-row .hover-display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.transaction-templates-table tr.transaction-templates-table-row:hover .hover-display {
|
||||
display: inline-grid;
|
||||
}
|
||||
|
||||
.transaction-templates-table tr:not(:last-child) > td > div {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.transaction-templates-table .has-bottom-border tr:last-child > td > div {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.transaction-templates-table tr.transaction-templates-table-row .right-bottom-icon .v-badge__badge {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.transaction-templates-table .v-text-field .v-input__prepend {
|
||||
margin-right: 0;
|
||||
color: rgba(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.transaction-templates-table .v-text-field .v-input__prepend .v-badge > .v-badge__wrapper > .v-icon {
|
||||
opacity: var(--v-medium-emphasis-opacity);
|
||||
}
|
||||
|
||||
.transaction-templates-table .v-text-field.v-input--plain-underlined .v-input__prepend {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.transaction-templates-table .v-text-field .v-field__input {
|
||||
font-size: 0.875rem;
|
||||
padding-top: 0;
|
||||
color: rgba(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.transaction-templates-table .transaction-template-name {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.transaction-templates-table tr .v-text-field .v-field__input {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
<v-progress-circular indeterminate size="22" class="ml-2" v-if="loading"></v-progress-circular>
|
||||
</div>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2" :icon="true"
|
||||
:disabled="loading || submitting" v-if="mode !== 'view'">
|
||||
:disabled="loading || submitting" v-if="mode !== 'view' && (mode !== 'editTemplate' || transaction.type === allTransactionTypes.Transfer)">
|
||||
<v-icon :icon="icons.more" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
@@ -24,13 +24,13 @@
|
||||
:title="$t('Swap Account and Amount')"
|
||||
v-if="transaction.type === allTransactionTypes.Transfer"
|
||||
@click="swapTransactionData(true, true)"></v-list-item>
|
||||
<v-divider v-if="transaction.type === allTransactionTypes.Transfer" />
|
||||
<v-divider v-if="mode !== 'editTemplate' && transaction.type === allTransactionTypes.Transfer" />
|
||||
<v-list-item :prepend-icon="icons.show"
|
||||
:title="$t('Show Amount')"
|
||||
v-if="transaction.hideAmount" @click="transaction.hideAmount = false"></v-list-item>
|
||||
v-if="mode !== 'editTemplate' && transaction.hideAmount" @click="transaction.hideAmount = false"></v-list-item>
|
||||
<v-list-item :prepend-icon="icons.hide"
|
||||
:title="$t('Hide Amount')"
|
||||
v-if="!transaction.hideAmount" @click="transaction.hideAmount = true"></v-list-item>
|
||||
v-if="mode !== 'editTemplate' && !transaction.hideAmount" @click="transaction.hideAmount = true"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
@@ -38,18 +38,18 @@
|
||||
</template>
|
||||
<v-card-text class="d-flex flex-column flex-md-row mt-md-4 pt-0">
|
||||
<div class="mb-4">
|
||||
<v-tabs class="v-tabs-pill" direction="vertical" :class="{ 'readonly': mode !== 'add' }"
|
||||
<v-tabs class="v-tabs-pill" direction="vertical" :class="{ 'readonly': mode !== 'add' && mode !== 'editTemplate' }"
|
||||
:disabled="loading || submitting" v-model="transaction.type">
|
||||
<v-tab :value="allTransactionTypes.Expense" :disabled="mode !== 'add' && transaction.type !== allTransactionTypes.Expense" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<v-tab :value="allTransactionTypes.Expense" :disabled="mode !== 'add' && mode !== 'editTemplate' && transaction.type !== allTransactionTypes.Expense" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<span>{{ $t('Expense') }}</span>
|
||||
</v-tab>
|
||||
<v-tab :value="allTransactionTypes.Income" :disabled="mode !== 'add' && transaction.type !== allTransactionTypes.Income" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<v-tab :value="allTransactionTypes.Income" :disabled="mode !== 'add' && mode !== 'editTemplate' && transaction.type !== allTransactionTypes.Income" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<span>{{ $t('Income') }}</span>
|
||||
</v-tab>
|
||||
<v-tab :value="allTransactionTypes.Transfer" :disabled="mode !== 'add' && transaction.type !== allTransactionTypes.Transfer" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<v-tab :value="allTransactionTypes.Transfer" :disabled="mode !== 'add' && mode !== 'editTemplate' && transaction.type !== allTransactionTypes.Transfer" v-if="transaction.type !== allTransactionTypes.ModifyBalance">
|
||||
<span>{{ $t('Transfer') }}</span>
|
||||
</v-tab>
|
||||
<v-tab :value="allTransactionTypes.ModifyBalance" v-if="transaction.type === allTransactionTypes.ModifyBalance">
|
||||
<v-tab :value="allTransactionTypes.ModifyBalance" v-if="mode !== 'editTemplate' && transaction.type === allTransactionTypes.ModifyBalance">
|
||||
<span>{{ $t('Modify Balance') }}</span>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
@@ -58,7 +58,7 @@
|
||||
<v-tab value="basicInfo">
|
||||
<span>{{ $t('Basic Information') }}</span>
|
||||
</v-tab>
|
||||
<v-tab value="map" :disabled="!transaction.geoLocation" v-if="mapProvider">
|
||||
<v-tab value="map" :disabled="!transaction.geoLocation" v-if="mode !== 'editTemplate' && mapProvider">
|
||||
<span>{{ $t('Location on Map') }}</span>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
@@ -177,7 +177,7 @@
|
||||
v-model="transaction.destinationAccountId">
|
||||
</two-column-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col cols="12" md="6" v-if="mode !== 'editTemplate'">
|
||||
<date-time-select
|
||||
:readonly="mode === 'view'"
|
||||
:disabled="loading || submitting"
|
||||
@@ -185,7 +185,7 @@
|
||||
v-model="transaction.time"
|
||||
@error="showDateTimeError" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col cols="12" md="6" v-if="mode !== 'editTemplate'">
|
||||
<v-autocomplete
|
||||
class="transaction-edit-timezone"
|
||||
item-title="displayNameWithUtcOffset"
|
||||
@@ -207,7 +207,7 @@
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-col cols="12" md="12" v-if="mode !== 'editTemplate'">
|
||||
<v-select
|
||||
persistent-placeholder
|
||||
:readonly="mode === 'view'"
|
||||
@@ -334,6 +334,7 @@ import { useAccountsStore } from '@/stores/account.js';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.js';
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.js';
|
||||
import { useTransactionsStore } from '@/stores/transaction.js';
|
||||
import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js';
|
||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
||||
|
||||
import categoryConstants from '@/consts/category.js';
|
||||
@@ -401,12 +402,14 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useUserStore, useAccountsStore, useTransactionCategoriesStore, useTransactionTagsStore, useTransactionsStore, useExchangeRatesStore),
|
||||
...mapStores(useSettingsStore, useUserStore, useAccountsStore, useTransactionCategoriesStore, useTransactionTagsStore, useTransactionsStore, useTransactionTemplatesStore, useExchangeRatesStore),
|
||||
title() {
|
||||
if (this.mode === 'add') {
|
||||
return 'Add Transaction';
|
||||
} else if (this.mode === 'edit') {
|
||||
return 'Edit Transaction';
|
||||
} else if (this.mode === 'editTemplate') {
|
||||
return 'Edit Transaction Template';
|
||||
} else {
|
||||
return 'Transaction Detail';
|
||||
}
|
||||
@@ -590,14 +593,14 @@ export default {
|
||||
}
|
||||
},
|
||||
'transaction.sourceAmount': function (newValue, oldValue) {
|
||||
if (this.mode === 'view') {
|
||||
if (this.mode === 'view' || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactionsStore.setTransactionSuitableDestinationAmount(this.transaction, oldValue, newValue);
|
||||
},
|
||||
'transaction.destinationAmount': function (newValue) {
|
||||
if (this.mode === 'view') {
|
||||
if (this.mode === 'view' || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -642,6 +645,15 @@ export default {
|
||||
self.editTransactionId = options.id;
|
||||
|
||||
promises.push(self.transactionsStore.getTransaction({ transactionId: self.editTransactionId }));
|
||||
} else if (options && options.editTemplateId) {
|
||||
if (options.currentTemplate) {
|
||||
self.setTransaction(options.currentTemplate, options, false, false);
|
||||
}
|
||||
|
||||
self.mode = 'editTemplate';
|
||||
self.transaction.id = options.editTemplateId;
|
||||
|
||||
promises.push(self.transactionTemplatesStore.getTemplate({ templateId: options.editTemplateId }));
|
||||
} else {
|
||||
self.mode = 'add';
|
||||
self.editTransactionId = null;
|
||||
@@ -671,10 +683,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.id && responses[3]) {
|
||||
if (options && options.id && responses[3]) {
|
||||
const transaction = responses[3];
|
||||
self.setTransaction(transaction, options, true, true);
|
||||
self.originalTransactionEditable = transaction.editable;
|
||||
} else if (options && options.editTemplateId && responses[3]) {
|
||||
const template = responses[3];
|
||||
self.setTransaction(template, options, false, false);
|
||||
} else {
|
||||
self.setTransaction(null, options, true, true);
|
||||
}
|
||||
@@ -701,31 +716,59 @@ export default {
|
||||
save() {
|
||||
const self = this;
|
||||
|
||||
if (self.mode === 'view') {
|
||||
return;
|
||||
}
|
||||
if (self.mode === 'add' || self.mode === 'edit') {
|
||||
const doSubmit = function () {
|
||||
self.submitting = true;
|
||||
|
||||
const doSubmit = function () {
|
||||
self.transactionsStore.saveTransaction({
|
||||
transaction: self.transaction,
|
||||
defaultCurrency: self.defaultCurrency,
|
||||
isEdit: self.mode === 'edit',
|
||||
clientSessionId: self.clientSessionId
|
||||
}).then(() => {
|
||||
self.submitting = false;
|
||||
|
||||
if (self.resolve) {
|
||||
if (self.mode === 'add') {
|
||||
self.resolve({
|
||||
message: 'You have added a new transaction'
|
||||
});
|
||||
} else if (self.mode === 'edit') {
|
||||
self.resolve({
|
||||
message: 'You have saved this transaction'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.showState = false;
|
||||
}).catch(error => {
|
||||
self.submitting = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (self.transaction.sourceAmount === 0) {
|
||||
self.$refs.confirmDialog.open('Are you sure you want to save this transaction with a zero amount?').then(() => {
|
||||
doSubmit();
|
||||
});
|
||||
} else {
|
||||
doSubmit();
|
||||
}
|
||||
} else if (self.mode === 'editTemplate') {
|
||||
self.submitting = true;
|
||||
|
||||
self.transactionsStore.saveTransaction({
|
||||
transaction: self.transaction,
|
||||
defaultCurrency: self.defaultCurrency,
|
||||
isEdit: self.mode === 'edit',
|
||||
clientSessionId: self.clientSessionId
|
||||
self.transactionTemplatesStore.modifyTemplateContent({
|
||||
template: self.transaction
|
||||
}).then(() => {
|
||||
self.submitting = false;
|
||||
|
||||
if (self.resolve) {
|
||||
if (self.mode === 'add') {
|
||||
self.resolve({
|
||||
message: 'You have added a new transaction'
|
||||
});
|
||||
} else if (self.mode === 'edit') {
|
||||
self.resolve({
|
||||
message: 'You have saved this transaction'
|
||||
});
|
||||
}
|
||||
self.resolve({
|
||||
message: 'You have saved this template'
|
||||
});
|
||||
}
|
||||
|
||||
self.showState = false;
|
||||
@@ -736,17 +779,13 @@ export default {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (self.transaction.sourceAmount === 0) {
|
||||
self.$refs.confirmDialog.open('Are you sure you want to save this transaction with a zero amount?').then(() => {
|
||||
doSubmit();
|
||||
});
|
||||
} else {
|
||||
doSubmit();
|
||||
}
|
||||
},
|
||||
duplicate() {
|
||||
if (this.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editTransactionId = null;
|
||||
this.transaction.id = null;
|
||||
this.transaction.time = getCurrentUnixTime();
|
||||
@@ -756,11 +795,19 @@ export default {
|
||||
this.mode = 'add';
|
||||
},
|
||||
edit() {
|
||||
if (this.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mode = 'edit';
|
||||
},
|
||||
remove() {
|
||||
const self = this;
|
||||
|
||||
if (self.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
self.$refs.confirmDialog.open('Are you sure you want to delete this transaction?').then(() => {
|
||||
self.submitting = true;
|
||||
|
||||
@@ -873,7 +920,11 @@ export default {
|
||||
type: options.type,
|
||||
categoryId: options.categoryId,
|
||||
accountId: options.accountId,
|
||||
tagIds: options.tagIds
|
||||
destinationAccountId: options.destinationAccountId,
|
||||
amount: options.amount,
|
||||
destinationAmount: options.destinationAmount,
|
||||
tagIds: options.tagIds,
|
||||
comment: options.comment
|
||||
},
|
||||
setContextData,
|
||||
convertContextTime
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<f7-page>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'f7router'
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user