mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 16:07:33 +08:00
support scheduled transaction (#2)
This commit is contained in:
@@ -154,6 +154,9 @@ duplicate_submissions_interval = 300
|
|||||||
# Set to true to clean up expired tokens periodically
|
# Set to true to clean up expired tokens periodically
|
||||||
enable_remove_expired_tokens = true
|
enable_remove_expired_tokens = true
|
||||||
|
|
||||||
|
# Set to true to create scheduled transactions based on the user's templates
|
||||||
|
enable_create_scheduled_transaction = true
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
||||||
secret_key =
|
secret_key =
|
||||||
@@ -196,6 +199,9 @@ enable_forget_password = true
|
|||||||
# Set to true to require email must be verified when use forget password
|
# Set to true to require email must be verified when use forget password
|
||||||
forget_password_require_email_verify = false
|
forget_password_require_email_verify = false
|
||||||
|
|
||||||
|
# Set to true to allow users to create scheduled transaction
|
||||||
|
enable_scheduled_transaction = true
|
||||||
|
|
||||||
# User avatar provider, supports the following types:
|
# User avatar provider, supports the following types:
|
||||||
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
|
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
|
||||||
# "gravatar": https://gravatar.com
|
# "gravatar": https://gravatar.com
|
||||||
|
|||||||
@@ -97,12 +97,20 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
|
|||||||
return nil, errs.ErrOperationFailed
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalScheduledTransactionCount, err := a.templates.GetTotalScheduledTemplateCountByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total scheduled transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
dataStatisticsResp := &models.DataStatisticsResponse{
|
dataStatisticsResp := &models.DataStatisticsResponse{
|
||||||
TotalAccountCount: totalAccountCount,
|
TotalAccountCount: totalAccountCount,
|
||||||
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
||||||
TotalTransactionTagCount: totalTransactionTagCount,
|
TotalTransactionTagCount: totalTransactionTagCount,
|
||||||
TotalTransactionCount: totalTransactionCount,
|
TotalTransactionCount: totalTransactionCount,
|
||||||
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
||||||
|
TotalScheduledTransactionCount: totalScheduledTransactionCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataStatisticsResp, nil
|
return dataStatisticsResp, nil
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
@@ -44,11 +45,15 @@ func (a *TransactionTemplatesApi) TemplateListHandler(c *core.WebContext) (any,
|
|||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
log.Warnf(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
|
log.Warnf(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
|
||||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
return nil, errs.ErrTransactionTemplateTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if templateListReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
||||||
|
|
||||||
@@ -87,6 +92,10 @@ func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.WebContext) (any, *
|
|||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||||
|
|
||||||
@@ -103,16 +112,34 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any
|
|||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
|
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
|
||||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
return nil, errs.ErrTransactionTemplateTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||||
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
|
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
|
||||||
return nil, errs.ErrTransactionTypeInvalid
|
return nil, errs.ErrTransactionTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
if templateCreateReq.ScheduledFrequencyType == nil ||
|
||||||
|
templateCreateReq.ScheduledFrequency == nil ||
|
||||||
|
templateCreateReq.ScheduledTimezoneUtcOffset == nil {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if *templateCreateReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency != "" {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
} else if *templateCreateReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency == "" {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
||||||
@@ -185,6 +212,24 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
|
|||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
if templateModifyReq.ScheduledFrequencyType == nil ||
|
||||||
|
templateModifyReq.ScheduledFrequency == nil ||
|
||||||
|
templateModifyReq.ScheduledTimezoneUtcOffset == nil {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if *templateModifyReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency != "" {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
} else if *templateModifyReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency == "" {
|
||||||
|
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newTemplate := &models.TransactionTemplate{
|
newTemplate := &models.TransactionTemplate{
|
||||||
TemplateId: template.TemplateId,
|
TemplateId: template.TemplateId,
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
@@ -200,6 +245,13 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
|
|||||||
Comment: templateModifyReq.Comment,
|
Comment: templateModifyReq.Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
newTemplate.ScheduledFrequencyType = *templateModifyReq.ScheduledFrequencyType
|
||||||
|
newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency)
|
||||||
|
newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||||
|
newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset
|
||||||
|
}
|
||||||
|
|
||||||
if newTemplate.Name == template.Name &&
|
if newTemplate.Name == template.Name &&
|
||||||
newTemplate.Type == template.Type &&
|
newTemplate.Type == template.Type &&
|
||||||
newTemplate.CategoryId == template.CategoryId &&
|
newTemplate.CategoryId == template.CategoryId &&
|
||||||
@@ -210,7 +262,16 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
|
|||||||
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
||||||
newTemplate.HideAmount == template.HideAmount &&
|
newTemplate.HideAmount == template.HideAmount &&
|
||||||
newTemplate.Comment == template.Comment {
|
newTemplate.Comment == template.Comment {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
||||||
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
|
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
|
||||||
|
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
|
||||||
|
newTemplate.ScheduledAt == template.ScheduledAt &&
|
||||||
|
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
|
||||||
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.templates.ModifyTemplate(c, newTemplate)
|
err = a.templates.ModifyTemplate(c, newTemplate)
|
||||||
@@ -242,6 +303,18 @@ func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.WebContext) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateHideReq.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateHideHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
|
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,6 +337,20 @@ func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.WebContext) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
|
if len(templateMoveReq.NewDisplayOrders) > 0 {
|
||||||
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateMoveReq.NewDisplayOrders[0].Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateMoveHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateMoveReq.NewDisplayOrders[0].Id, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
|
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
|
||||||
|
|
||||||
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
||||||
@@ -299,6 +386,18 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateDeleteReq.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateDeleteHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
|
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -311,7 +410,7 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
||||||
return &models.TransactionTemplate{
|
template := &models.TransactionTemplate{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
TemplateType: templateCreateReq.TemplateType,
|
TemplateType: templateCreateReq.TemplateType,
|
||||||
Name: templateCreateReq.Name,
|
Name: templateCreateReq.Name,
|
||||||
@@ -326,4 +425,60 @@ func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCrea
|
|||||||
Comment: templateCreateReq.Comment,
|
Comment: templateCreateReq.Comment,
|
||||||
DisplayOrder: order,
|
DisplayOrder: order,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
template.ScheduledFrequencyType = *templateCreateReq.ScheduledFrequencyType
|
||||||
|
template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency)
|
||||||
|
template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||||
|
template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 {
|
||||||
|
templateTimeZone := time.FixedZone("Template Timezone", int(scheduledTimezoneUtcOffset)*60)
|
||||||
|
transactionTime := time.Date(2020, 1, 1, 0, 0, 0, 0, templateTimeZone)
|
||||||
|
transactionTimeInUTC := transactionTime.In(time.UTC)
|
||||||
|
|
||||||
|
minutesElapsedOfDayInUtc := transactionTimeInUTC.Hour()*60 + transactionTimeInUTC.Minute()
|
||||||
|
|
||||||
|
return int16(minutesElapsedOfDayInUtc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TransactionTemplatesApi) getOrderedFrequencyValues(frequencyValue string) string {
|
||||||
|
if frequencyValue == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
items := strings.Split(frequencyValue, ",")
|
||||||
|
values := make([]int, 0, len(items))
|
||||||
|
valueExistMap := make(map[int]bool)
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i++ {
|
||||||
|
value, err := utils.StringToInt(items[i])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := valueExistMap[value]; !exists {
|
||||||
|
values = append(values, value)
|
||||||
|
valueExistMap[value] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(values)
|
||||||
|
|
||||||
|
var sortedFrequencyValueBuilder strings.Builder
|
||||||
|
|
||||||
|
for i := 0; i < len(values); i++ {
|
||||||
|
if sortedFrequencyValueBuilder.Len() > 0 {
|
||||||
|
sortedFrequencyValueBuilder.WriteRune(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedFrequencyValueBuilder.WriteString(utils.IntToString(values[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedFrequencyValueBuilder.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import (
|
|||||||
// CronContext represents the cron job context
|
// CronContext represents the cron job context
|
||||||
type CronContext struct {
|
type CronContext struct {
|
||||||
context.Context
|
context.Context
|
||||||
contextId string
|
contextId string
|
||||||
|
cronJobInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContextId returns the current context id
|
// GetContextId returns the current context id
|
||||||
@@ -18,11 +19,17 @@ func (c *CronContext) GetContextId() string {
|
|||||||
return c.contextId
|
return c.contextId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterval returns the current cron job interval
|
||||||
|
func (c *CronContext) GetInterval() time.Duration {
|
||||||
|
return c.cronJobInterval
|
||||||
|
}
|
||||||
|
|
||||||
// NewCronJobContext returns a new cron job context
|
// NewCronJobContext returns a new cron job context
|
||||||
func NewCronJobContext(cronJobName string) *CronContext {
|
func NewCronJobContext(cronJobName string, cronJobInterval time.Duration) *CronContext {
|
||||||
return &CronContext{
|
return &CronContext{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
contextId: generateNewRandomCronContextId(cronJobName),
|
contextId: generateNewRandomCronContextId(cronJobName),
|
||||||
|
cronJobInterval: cronJobInterval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ func (c *CronJobSchedulerContainer) registerAllJobs(ctx core.Context, config *se
|
|||||||
if config.EnableRemoveExpiredTokens {
|
if config.EnableRemoveExpiredTokens {
|
||||||
Container.registerIntervalJob(ctx, RemoveExpiredTokensJob)
|
Container.registerIntervalJob(ctx, RemoveExpiredTokensJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.EnableCreateScheduledTransaction {
|
||||||
|
Container.registerIntervalJob(ctx, CreateScheduledTransactionJob)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CronJobSchedulerContainer) registerIntervalJob(ctx core.Context, job *CronJob) {
|
func (c *CronJobSchedulerContainer) registerIntervalJob(ctx core.Context, job *CronJob) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type CronJob struct {
|
|||||||
|
|
||||||
func (j *CronJob) doRun() {
|
func (j *CronJob) doRun() {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
c := core.NewCronJobContext(j.Name)
|
c := core.NewCronJobContext(j.Name, j.Period.GetInterval())
|
||||||
|
|
||||||
if duplicatechecker.Container.Current != nil {
|
if duplicatechecker.Container.Current != nil {
|
||||||
localAddr, err := utils.GetLocalIPAddressesString()
|
localAddr, err := utils.GetLocalIPAddressesString()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cron
|
package cron
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-co-op/gocron/v2"
|
"github.com/go-co-op/gocron/v2"
|
||||||
@@ -22,6 +23,11 @@ type CronJobFixedHourPeriod struct {
|
|||||||
Hour uint32
|
Hour uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CronJobEvery15MinutesPeriod represents the period of execution at every 15 minutes
|
||||||
|
type CronJobEvery15MinutesPeriod struct {
|
||||||
|
Second uint32
|
||||||
|
}
|
||||||
|
|
||||||
// CronJobFixedTimePeriod represents the period of execution at fixed time
|
// CronJobFixedTimePeriod represents the period of execution at fixed time
|
||||||
type CronJobFixedTimePeriod struct {
|
type CronJobFixedTimePeriod struct {
|
||||||
Time time.Time
|
Time time.Time
|
||||||
@@ -52,6 +58,16 @@ func (p CronJobFixedHourPeriod) ToJobDefinition() gocron.JobDefinition {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterval returns the interval time of the period of CronJobEvery15MinutesPeriod
|
||||||
|
func (p CronJobEvery15MinutesPeriod) GetInterval() time.Duration {
|
||||||
|
return 15 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJobDefinition returns the gocron job definition of the period of CronJobEvery15MinutesPeriod
|
||||||
|
func (p CronJobEvery15MinutesPeriod) ToJobDefinition() gocron.JobDefinition {
|
||||||
|
return gocron.CronJob(fmt.Sprintf("%d */15 * * * *", p.Second), true)
|
||||||
|
}
|
||||||
|
|
||||||
// GetInterval returns the interval time of the period of CronJobFixedTimePeriod
|
// GetInterval returns the interval time of the period of CronJobFixedTimePeriod
|
||||||
func (p CronJobFixedTimePeriod) GetInterval() time.Duration {
|
func (p CronJobFixedTimePeriod) GetInterval() time.Duration {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -97,6 +97,61 @@ func TestCronJobNextRunTimeWithFixedHourPeriod(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCronJobNextRunTimeWithEvery15MinutesPeriod(t *testing.T) {
|
||||||
|
scheduler, err := gocron.NewScheduler(
|
||||||
|
gocron.WithLocation(time.Local),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
expectedSecond := uint32(23)
|
||||||
|
|
||||||
|
job := CronJob{
|
||||||
|
Name: "TestCronJobWithEvery15MinutesPeriod",
|
||||||
|
Description: "The test cron job",
|
||||||
|
Period: CronJobEvery15MinutesPeriod{
|
||||||
|
Second: expectedSecond,
|
||||||
|
},
|
||||||
|
Run: func(c *core.CronContext) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 15*time.Minute, job.Period.GetInterval())
|
||||||
|
|
||||||
|
gocronJob, err := scheduler.NewJob(
|
||||||
|
job.Period.ToJobDefinition(),
|
||||||
|
gocron.NewTask(job.doRun),
|
||||||
|
gocron.WithName(job.Name),
|
||||||
|
gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
scheduler.Start()
|
||||||
|
|
||||||
|
nextRunTime, err := gocronJob.NextRun()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
nextMinuteTime := time.Now()
|
||||||
|
|
||||||
|
if (nextMinuteTime.Minute() == 0 || nextMinuteTime.Minute() == 15 || nextMinuteTime.Minute() == 30 || nextMinuteTime.Minute() == 45) && nextMinuteTime.Second() < int(expectedSecond) {
|
||||||
|
// Do Nothing
|
||||||
|
} else {
|
||||||
|
nextMinute := ((nextMinuteTime.Minute() / 15) + 1) * 15
|
||||||
|
minuteDiff := nextMinute - nextMinuteTime.Minute()
|
||||||
|
nextMinuteTime = nextMinuteTime.Add(time.Duration(int64(minuteDiff) * int64(time.Minute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, nextMinuteTime.Year(), nextRunTime.Year())
|
||||||
|
assert.Equal(t, nextMinuteTime.Month(), nextRunTime.Month())
|
||||||
|
assert.Equal(t, nextMinuteTime.Day(), nextRunTime.Day())
|
||||||
|
assert.Equal(t, nextMinuteTime.Hour(), nextRunTime.Hour())
|
||||||
|
assert.Equal(t, nextMinuteTime.Minute(), nextRunTime.Minute())
|
||||||
|
assert.Equal(t, int(expectedSecond), nextRunTime.Second())
|
||||||
|
|
||||||
|
err = scheduler.Shutdown()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCronJobNextRunTimeWithFixedTimePeriod(t *testing.T) {
|
func TestCronJobNextRunTimeWithFixedTimePeriod(t *testing.T) {
|
||||||
scheduler, err := gocron.NewScheduler(
|
scheduler, err := gocron.NewScheduler(
|
||||||
gocron.WithLocation(time.Local),
|
gocron.WithLocation(time.Local),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package cron
|
package cron
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
)
|
)
|
||||||
@@ -16,3 +18,15 @@ var RemoveExpiredTokensJob = &CronJob{
|
|||||||
return services.Tokens.DeleteAllExpiredTokens(c)
|
return services.Tokens.DeleteAllExpiredTokens(c)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateScheduledTransactionJob represents the cron job which periodically create transaction by scheduled transaction template
|
||||||
|
var CreateScheduledTransactionJob = &CronJob{
|
||||||
|
Name: "CreateScheduledTransaction",
|
||||||
|
Description: "Periodically create transaction by scheduled transaction template.",
|
||||||
|
Period: CronJobEvery15MinutesPeriod{
|
||||||
|
Second: 0,
|
||||||
|
},
|
||||||
|
Run: func(c *core.CronContext) error {
|
||||||
|
return services.Transactions.CreateScheduledTransactions(c, time.Now().Unix(), c.GetInterval())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction templates
|
// Error codes related to transaction templates
|
||||||
var (
|
var (
|
||||||
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
|
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
|
||||||
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
|
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
|
||||||
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
|
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
|
||||||
|
ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled")
|
||||||
|
ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
|||||||
buildBooleanSetting("r", config.EnableUserRegister),
|
buildBooleanSetting("r", config.EnableUserRegister),
|
||||||
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
||||||
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
||||||
|
buildBooleanSetting("s", config.EnableScheduledTransaction),
|
||||||
buildBooleanSetting("e", config.EnableDataExport),
|
buildBooleanSetting("e", config.EnableDataExport),
|
||||||
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ type ClearDataRequest struct {
|
|||||||
|
|
||||||
// DataStatisticsResponse represents a view-object of user data statistic
|
// DataStatisticsResponse represents a view-object of user data statistic
|
||||||
type DataStatisticsResponse struct {
|
type DataStatisticsResponse struct {
|
||||||
TotalAccountCount int64 `json:"totalAccountCount,string"`
|
TotalAccountCount int64 `json:"totalAccountCount,string"`
|
||||||
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
|
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
|
||||||
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
|
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
|
||||||
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
|
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
|
||||||
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
|
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
|
||||||
|
TotalScheduledTransactionCount int64 `json:"totalScheduledTransactionCount,string"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type Transaction struct {
|
|||||||
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
|
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
|
||||||
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
|
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
|
||||||
CreatedIp string `xorm:"VARCHAR(39)"`
|
CreatedIp string `xorm:"VARCHAR(39)"`
|
||||||
|
ScheduledCreated bool
|
||||||
CreatedUnixTime int64
|
CreatedUnixTime int64
|
||||||
UpdatedUnixTime int64
|
UpdatedUnixTime int64
|
||||||
DeletedUnixTime int64
|
DeletedUnixTime int64
|
||||||
|
|||||||
@@ -11,30 +11,45 @@ type TransactionTemplateType byte
|
|||||||
|
|
||||||
// Transaction template types
|
// Transaction template types
|
||||||
const (
|
const (
|
||||||
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
|
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
|
||||||
|
TRANSACTION_TEMPLATE_TYPE_SCHEDULE TransactionTemplateType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransactionScheduleFrequencyType represents transaction template schedule frequency type
|
||||||
|
type TransactionScheduleFrequencyType byte
|
||||||
|
|
||||||
|
// Transaction template schedule frequency types
|
||||||
|
const (
|
||||||
|
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED TransactionScheduleFrequencyType = 0
|
||||||
|
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY TransactionScheduleFrequencyType = 1
|
||||||
|
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY TransactionScheduleFrequencyType = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransactionTemplate represents transaction template stored in database
|
// TransactionTemplate represents transaction template stored in database
|
||||||
type TransactionTemplate struct {
|
type TransactionTemplate struct {
|
||||||
TemplateId int64 `xorm:"PK"`
|
TemplateId int64 `xorm:"PK"`
|
||||||
Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
Uid int64 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"`
|
||||||
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
|
||||||
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
|
||||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
||||||
Type TransactionType `xorm:"NOT NULL"`
|
Type TransactionType `xorm:"NOT NULL"`
|
||||||
CategoryId int64 `xorm:"NOT NULL"`
|
CategoryId int64 `xorm:"NOT NULL"`
|
||||||
AccountId int64 `xorm:"NOT NULL"`
|
AccountId int64 `xorm:"NOT NULL"`
|
||||||
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
|
ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
|
||||||
Amount int64 `xorm:"NOT NULL"`
|
ScheduledFrequency string `xorm:"VARCHAR(100)"`
|
||||||
RelatedAccountId int64 `xorm:"NOT NULL"`
|
ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
|
||||||
RelatedAccountAmount int64 `xorm:"NOT NULL"`
|
ScheduledTimezoneUtcOffset int16
|
||||||
HideAmount bool `xorm:"NOT NULL"`
|
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
|
||||||
Comment string `xorm:"VARCHAR(255) NOT NULL"`
|
Amount int64 `xorm:"NOT NULL"`
|
||||||
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
|
RelatedAccountId int64 `xorm:"NOT NULL"`
|
||||||
Hidden bool `xorm:"NOT NULL"`
|
RelatedAccountAmount int64 `xorm:"NOT NULL"`
|
||||||
CreatedUnixTime int64
|
HideAmount bool `xorm:"NOT NULL"`
|
||||||
UpdatedUnixTime int64
|
Comment string `xorm:"VARCHAR(255) NOT NULL"`
|
||||||
DeletedUnixTime int64
|
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_template_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
|
// TransactionTemplateListRequest represents all parameters of transaction template list request
|
||||||
@@ -49,18 +64,21 @@ type TransactionTemplateGetRequest struct {
|
|||||||
|
|
||||||
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
|
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
|
||||||
type TransactionTemplateCreateRequest struct {
|
type TransactionTemplateCreateRequest struct {
|
||||||
TemplateType TransactionTemplateType `json:"templateType"`
|
TemplateType TransactionTemplateType `json:"templateType"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=32"`
|
||||||
Type TransactionType `json:"type" binding:"required"`
|
Type TransactionType `json:"type" binding:"required"`
|
||||||
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
||||||
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
||||||
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
|
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
|
||||||
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
|
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
|
||||||
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
|
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
|
||||||
HideAmount bool `json:"hideAmount"`
|
HideAmount bool `json:"hideAmount"`
|
||||||
TagIds []string `json:"tagIds"`
|
TagIds []string `json:"tagIds"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
ClientSessionId string `json:"clientSessionId"`
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
||||||
|
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
||||||
|
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
||||||
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
|
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
|
||||||
@@ -71,17 +89,20 @@ type TransactionTemplateModifyNameRequest struct {
|
|||||||
|
|
||||||
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
|
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
|
||||||
type TransactionTemplateModifyRequest struct {
|
type TransactionTemplateModifyRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=32"`
|
||||||
Type TransactionType `json:"type" binding:"required"`
|
Type TransactionType `json:"type" binding:"required"`
|
||||||
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
||||||
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
||||||
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
|
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
|
||||||
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
|
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
|
||||||
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
|
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
|
||||||
HideAmount bool `json:"hideAmount"`
|
HideAmount bool `json:"hideAmount"`
|
||||||
TagIds []string `json:"tagIds"`
|
TagIds []string `json:"tagIds"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
||||||
|
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
||||||
|
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
|
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
|
||||||
@@ -108,14 +129,54 @@ type TransactionTemplateDeleteRequest struct {
|
|||||||
|
|
||||||
type TransactionTemplateInfoResponse struct {
|
type TransactionTemplateInfoResponse struct {
|
||||||
*TransactionInfoResponse
|
*TransactionInfoResponse
|
||||||
TemplateType TransactionTemplateType `json:"templateType"`
|
TemplateType TransactionTemplateType `json:"templateType"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisplayOrder int32 `json:"displayOrder"`
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType,omitempty"`
|
||||||
Hidden bool `json:"hidden"`
|
ScheduledFrequency *string `json:"scheduledFrequency,omitempty"`
|
||||||
|
ScheduledAt *int16 `json:"scheduledAt,omitempty"`
|
||||||
|
DisplayOrder int32 `json:"displayOrder"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToTransactionInfoResponse returns a view-object according to database model
|
// GetTagIds returns all tag ids of the transaction template
|
||||||
func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
|
func (t *TransactionTemplate) GetTagIds() []int64 {
|
||||||
|
tagIds := make([]string, 0)
|
||||||
|
|
||||||
|
if t.TagIds != "" {
|
||||||
|
tagIds = strings.Split(t.TagIds, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := utils.StringArrayToInt64Array(tagIds)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTransactionTemplateInfoResponse returns a view-object according to database model
|
||||||
|
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(serverUtcOffset int16) *TransactionTemplateInfoResponse {
|
||||||
|
utcOffset := serverUtcOffset
|
||||||
|
|
||||||
|
if t.TemplateType == TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
utcOffset = t.ScheduledTimezoneUtcOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &TransactionTemplateInfoResponse{
|
||||||
|
TransactionInfoResponse: t.toTransactionInfoResponse(utcOffset),
|
||||||
|
TemplateType: t.TemplateType,
|
||||||
|
Name: t.Name,
|
||||||
|
DisplayOrder: t.DisplayOrder,
|
||||||
|
Hidden: t.Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.TemplateType == TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
response.ScheduledFrequencyType = &t.ScheduledFrequencyType
|
||||||
|
response.ScheduledFrequency = &t.ScheduledFrequency
|
||||||
|
response.ScheduledAt = &t.ScheduledAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TransactionTemplate) toTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
|
||||||
tagIds := make([]string, 0, 0)
|
tagIds := make([]string, 0, 0)
|
||||||
|
|
||||||
if t.TagIds != "" {
|
if t.TagIds != "" {
|
||||||
@@ -141,17 +202,6 @@ func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *Transa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse
|
||||||
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
|
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -26,7 +26,7 @@ func (s *ServiceUsingDB) TokenDB(uid int64) *datastore.Database {
|
|||||||
return s.container.TokenStore.Choose(uid)
|
return s.container.TokenStore.Choose(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenDBByIndex returns the datastore by index
|
// TokenDBByIndex returns the datastore which contains user token by index
|
||||||
func (s *ServiceUsingDB) TokenDBByIndex(index int) *datastore.Database {
|
func (s *ServiceUsingDB) TokenDBByIndex(index int) *datastore.Database {
|
||||||
return s.container.TokenStore.Get(index)
|
return s.container.TokenStore.Get(index)
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,16 @@ func (s *ServiceUsingDB) UserDataDB(uid int64) *datastore.Database {
|
|||||||
return s.container.UserDataStore.Choose(uid)
|
return s.container.UserDataStore.Choose(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserDataDBByIndex returns the datastore which contains user data by index
|
||||||
|
func (s *ServiceUsingDB) UserDataDBByIndex(index int) *datastore.Database {
|
||||||
|
return s.container.UserDataStore.Get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDataDBCount returns the count of datastores which contains user data
|
||||||
|
func (s *ServiceUsingDB) UserDataDBCount() int {
|
||||||
|
return s.container.UserDataStore.Count()
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceUsingConfig represents a service that need to use config
|
// ServiceUsingConfig represents a service that need to use config
|
||||||
type ServiceUsingConfig struct {
|
type ServiceUsingConfig struct {
|
||||||
container *settings.ConfigContainer
|
container *settings.ConfigContainer
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTotalNormalTemplateCountByUid returns total template count of user
|
// GetTotalNormalTemplateCountByUid returns total normal template count of user
|
||||||
func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c core.Context, uid int64) (int64, error) {
|
func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c core.Context, uid int64) (int64, error) {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
return 0, errs.ErrUserIdInvalid
|
return 0, errs.ErrUserIdInvalid
|
||||||
@@ -40,6 +40,17 @@ func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c core.Con
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTotalScheduledTemplateCountByUid returns total scheduled transaction count of user
|
||||||
|
func (s *TransactionTemplateService) GetTotalScheduledTemplateCountByUid(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=? AND template_type=?", uid, false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE).Count(&models.TransactionTemplate{})
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllTemplatesByUid returns all transaction template models of user
|
// GetAllTemplatesByUid returns all transaction template models of user
|
||||||
func (s *TransactionTemplateService) GetAllTemplatesByUid(c core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
|
func (s *TransactionTemplateService) GetAllTemplatesByUid(c core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
@@ -125,7 +136,7 @@ func (s *TransactionTemplateService) ModifyTemplate(c core.Context, template *mo
|
|||||||
template.UpdatedUnixTime = time.Now().Unix()
|
template.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||||
@@ -429,6 +430,135 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateScheduledTransactions saves all scheduled transactions that should be created now
|
||||||
|
func (s *TransactionService) CreateScheduledTransactions(c core.Context, currentUnixTime int64, interval time.Duration) error {
|
||||||
|
var allTemplates []*models.TransactionTemplate
|
||||||
|
intervalMinute := int(interval / time.Minute)
|
||||||
|
currentTime := time.Unix(currentUnixTime, 0)
|
||||||
|
currentMinute := (currentTime.Minute() / intervalMinute) * intervalMinute
|
||||||
|
|
||||||
|
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), currentTime.Hour(), currentMinute, 0, 0, time.Local)
|
||||||
|
startTimeInUTC := startTime.In(time.UTC)
|
||||||
|
|
||||||
|
minutesElapsedOfDayInUtc := startTimeInUTC.Hour()*60 + startTimeInUTC.Minute()
|
||||||
|
secondsElapsedOfDayInUtc := minutesElapsedOfDayInUtc * 60
|
||||||
|
todayFirstTimeInUTC := startTimeInUTC.Add(time.Duration(-secondsElapsedOfDayInUtc) * time.Second)
|
||||||
|
todayFirstUnixTimeInUTC := todayFirstTimeInUTC.Unix()
|
||||||
|
|
||||||
|
minScheduledAt := minutesElapsedOfDayInUtc
|
||||||
|
maxScheduledAt := minScheduledAt + intervalMinute
|
||||||
|
|
||||||
|
for i := 0; i < s.UserDataDBCount(); i++ {
|
||||||
|
var templates []*models.TransactionTemplate
|
||||||
|
err := s.UserDataDBByIndex(i).NewSession(c).Where("deleted=? AND template_type=? AND (scheduled_frequency_type=? OR scheduled_frequency_type=?) AND scheduled_at>=? AND scheduled_at<?", false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY, minScheduledAt, maxScheduledAt).Find(&templates)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allTemplates = append(allTemplates, templates...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allTemplates) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] should process %d scheduled transaction templates now (scheduled at from %d to %d)", len(allTemplates), minScheduledAt, maxScheduledAt)
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
skipCount := 0
|
||||||
|
failedCount := 0
|
||||||
|
|
||||||
|
for i := 0; i < len(allTemplates); i++ {
|
||||||
|
template := allTemplates[i]
|
||||||
|
|
||||||
|
if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED {
|
||||||
|
skipCount++
|
||||||
|
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" disabled scheduled transaction frequency", template.TemplateId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY &&
|
||||||
|
template.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY) ||
|
||||||
|
template.ScheduledFrequency == "" {
|
||||||
|
skipCount++
|
||||||
|
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid scheduled transaction frequency", template.TemplateId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
frequencyValues, err := utils.StringArrayToInt64Array(strings.Split(template.ScheduledFrequency, ","))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
skipCount++
|
||||||
|
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid scheduled transaction frequency, because %s", template.TemplateId, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
frequencyValueSet := utils.ToSet(frequencyValues)
|
||||||
|
templateTimeZone := time.FixedZone("Template Timezone", int(template.ScheduledTimezoneUtcOffset)*60)
|
||||||
|
transactionUnixTime := todayFirstUnixTimeInUTC + int64(template.ScheduledAt)*60
|
||||||
|
transactionTime := time.Unix(transactionUnixTime, 0).In(templateTimeZone)
|
||||||
|
|
||||||
|
if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY && !frequencyValueSet[int64(transactionTime.Weekday())] {
|
||||||
|
skipCount++
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, today is %s", template.TemplateId, startTimeInUTC.Weekday())
|
||||||
|
continue
|
||||||
|
} else if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY && !frequencyValueSet[int64(transactionTime.Day())] {
|
||||||
|
skipCount++
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, today is %d of month", template.TemplateId, startTimeInUTC.Day())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactionDbType models.TransactionDbType
|
||||||
|
|
||||||
|
if template.Type == models.TRANSACTION_TYPE_EXPENSE {
|
||||||
|
transactionDbType = models.TRANSACTION_DB_TYPE_EXPENSE
|
||||||
|
} else if template.Type == models.TRANSACTION_TYPE_INCOME {
|
||||||
|
transactionDbType = models.TRANSACTION_DB_TYPE_INCOME
|
||||||
|
} else if template.Type == models.TRANSACTION_TYPE_TRANSFER {
|
||||||
|
transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
|
||||||
|
} else {
|
||||||
|
skipCount++
|
||||||
|
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid transaction type", template.TemplateId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction := &models.Transaction{
|
||||||
|
Uid: template.Uid,
|
||||||
|
Type: transactionDbType,
|
||||||
|
CategoryId: template.CategoryId,
|
||||||
|
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||||
|
TimezoneUtcOffset: template.ScheduledTimezoneUtcOffset,
|
||||||
|
AccountId: template.AccountId,
|
||||||
|
Amount: template.Amount,
|
||||||
|
HideAmount: template.HideAmount,
|
||||||
|
Comment: template.Comment,
|
||||||
|
CreatedIp: "127.0.0.1",
|
||||||
|
ScheduledCreated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.Type == models.TRANSACTION_TYPE_TRANSFER {
|
||||||
|
transaction.RelatedAccountId = template.RelatedAccountId
|
||||||
|
transaction.RelatedAccountAmount = template.RelatedAccountAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
tagIds := template.GetTagIds()
|
||||||
|
err = s.CreateTransaction(c, transaction, tagIds)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
successCount++
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has created a new trasaction \"id:%d\"", template.TemplateId, transaction.TransactionId)
|
||||||
|
} else {
|
||||||
|
failedCount++
|
||||||
|
log.Errorf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" failed to create new trasaction", template.TemplateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] %d transactions has been created successfully, %d templates does not need to create transactions and %d transactions failed to create", successCount, skipCount, failedCount)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ModifyTransaction saves an existed transaction to database
|
// ModifyTransaction saves an existed transaction to database
|
||||||
func (s *TransactionService) ModifyTransaction(c core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error {
|
func (s *TransactionService) ModifyTransaction(c core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error {
|
||||||
if transaction.Uid <= 0 {
|
if transaction.Uid <= 0 {
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ type Config struct {
|
|||||||
|
|
||||||
// Cron
|
// Cron
|
||||||
EnableRemoveExpiredTokens bool
|
EnableRemoveExpiredTokens bool
|
||||||
|
EnableCreateScheduledTransaction bool
|
||||||
|
|
||||||
// Secret
|
// Secret
|
||||||
SecretKeyNoSet bool
|
SecretKeyNoSet bool
|
||||||
@@ -270,6 +271,7 @@ type Config struct {
|
|||||||
EnableUserForceVerifyEmail bool
|
EnableUserForceVerifyEmail bool
|
||||||
EnableUserForgetPassword bool
|
EnableUserForgetPassword bool
|
||||||
ForgetPasswordRequireVerifyEmail bool
|
ForgetPasswordRequireVerifyEmail bool
|
||||||
|
EnableScheduledTransaction bool
|
||||||
AvatarProvider core.UserAvatarProviderType
|
AvatarProvider core.UserAvatarProviderType
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
@@ -679,6 +681,7 @@ func loadDuplicateCheckerConfiguration(config *Config, configFile *ini.File, sec
|
|||||||
|
|
||||||
func loadCronConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
func loadCronConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||||
config.EnableRemoveExpiredTokens = getConfigItemBoolValue(configFile, sectionName, "enable_remove_expired_tokens", false)
|
config.EnableRemoveExpiredTokens = getConfigItemBoolValue(configFile, sectionName, "enable_remove_expired_tokens", false)
|
||||||
|
config.EnableCreateScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_create_scheduled_transaction", false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -737,6 +740,7 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
|
|||||||
config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
|
config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
|
||||||
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||||
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
||||||
|
config.EnableScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_scheduled_transaction", false)
|
||||||
|
|
||||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) {
|
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) {
|
||||||
config.AvatarProvider = core.USER_AVATAR_PROVIDER_INTERNAL
|
config.AvatarProvider = core.USER_AVATAR_PROVIDER_INTERNAL
|
||||||
|
|||||||
@@ -57,3 +57,15 @@ func ToUniqueInt64Slice(items []int64) []int64 {
|
|||||||
|
|
||||||
return uniqueItems
|
return uniqueItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToSet returns a map where the keys are the items in the specified array
|
||||||
|
func ToSet(items []int64) map[int64]bool {
|
||||||
|
itemExistMap := make(map[int64]bool)
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i++ {
|
||||||
|
item := items[i]
|
||||||
|
itemExistMap[item] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemExistMap
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,3 +130,25 @@ func TestToUniqueInt64Slice_NilOrEmpty(t *testing.T) {
|
|||||||
actualValue = ToUniqueInt64Slice(arr)
|
actualValue = ToUniqueInt64Slice(arr)
|
||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToSet(t *testing.T) {
|
||||||
|
arr := []int64{0, 1, 2, 3, 2, 4, 0}
|
||||||
|
actualValue := ToSet(arr)
|
||||||
|
assert.Equal(t, 5, len(actualValue))
|
||||||
|
assert.Equal(t, true, actualValue[0])
|
||||||
|
assert.Equal(t, true, actualValue[1])
|
||||||
|
assert.Equal(t, true, actualValue[2])
|
||||||
|
assert.Equal(t, true, actualValue[3])
|
||||||
|
assert.Equal(t, true, actualValue[4])
|
||||||
|
assert.Equal(t, false, actualValue[5])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSet_NilOrEmpty(t *testing.T) {
|
||||||
|
var arr []int64 = nil
|
||||||
|
actualValue := ToSet(arr)
|
||||||
|
assert.Equal(t, 0, len(actualValue))
|
||||||
|
|
||||||
|
arr = []int64{}
|
||||||
|
actualValue = ToSet(arr)
|
||||||
|
assert.Equal(t, 0, len(actualValue))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
persistent-placeholder
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:label="label"
|
||||||
|
:menu-props="{ 'content-class': 'schedule-frequency-select-menu' }"
|
||||||
|
v-model="frequencyType"
|
||||||
|
v-model:menu="menuState"
|
||||||
|
@update:menu="onMenuStateChanged"
|
||||||
|
>
|
||||||
|
<template #selection>
|
||||||
|
<span class="text-truncate cursor-pointer">{{ displayFrequency }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #no-data>
|
||||||
|
<div ref="dropdownMenu" class="schedule-frequency-container">
|
||||||
|
<div class="schedule-frequency-type-container">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item :class="{ 'v-list-item--active text-primary': type.type === frequencyType }"
|
||||||
|
:key="type.type" :title="type.displayName"
|
||||||
|
v-for="type in allTransactionScheduledFrequencyTypes"
|
||||||
|
@click="frequencyType = type.type">
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
<div class="schedule-frequency-value-container">
|
||||||
|
<v-list v-if="frequencyType === allTemplateScheduledFrequencyTypes.Disabled.type">
|
||||||
|
<v-list-item :title="$t('None')"></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
||||||
|
v-else-if="frequencyType === allTemplateScheduledFrequencyTypes.Weekly.type">
|
||||||
|
<v-list-item :key="weekDay.type" :value="weekDay.type" :title="weekDay.displayName"
|
||||||
|
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(weekDay.type) }"
|
||||||
|
v-for="weekDay in allWeekDays">
|
||||||
|
<template v-slot:prepend="{ isActive }">
|
||||||
|
<v-checkbox density="compact" class="mr-1" :model-value="isActive"></v-checkbox>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
||||||
|
v-else-if="frequencyType === allTemplateScheduledFrequencyTypes.Monthly.type">
|
||||||
|
<v-list-item :key="monthDay.day" :value="monthDay.day" :title="monthDay.displayName"
|
||||||
|
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(monthDay.day) }"
|
||||||
|
v-for="monthDay in allAvailableMonthDays">
|
||||||
|
<template v-slot:prepend="{ isActive }">
|
||||||
|
<v-checkbox density="compact" class="mr-1" :model-value="isActive"></v-checkbox>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
|
import { sortNumbersArray } from '@/lib/common.js';
|
||||||
|
import { scrollToSelectedItem } from '@/lib/ui.desktop.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'type',
|
||||||
|
'modelValue',
|
||||||
|
'disabled',
|
||||||
|
'readonly',
|
||||||
|
'label'
|
||||||
|
],
|
||||||
|
emits: [
|
||||||
|
'update:type',
|
||||||
|
'update:modelValue'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menuState: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useUserStore),
|
||||||
|
allTransactionScheduledFrequencyTypes() {
|
||||||
|
return this.$locale.getAllTransactionScheduledFrequencyTypes();
|
||||||
|
},
|
||||||
|
allTemplateScheduledFrequencyTypes() {
|
||||||
|
return templateConstants.allTemplateScheduledFrequencyTypes;
|
||||||
|
},
|
||||||
|
allWeekDays() {
|
||||||
|
return this.$locale.getAllWeekDays(this.firstDayOfWeek);
|
||||||
|
},
|
||||||
|
allAvailableMonthDays() {
|
||||||
|
const allAvailableDays = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 28; i++) {
|
||||||
|
allAvailableDays.push({
|
||||||
|
day: i,
|
||||||
|
displayName: this.$locale.getMonthdayShortName(i),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAvailableDays;
|
||||||
|
},
|
||||||
|
firstDayOfWeek() {
|
||||||
|
return this.userStore.currentUserFirstDayOfWeek;
|
||||||
|
},
|
||||||
|
frequencyType: {
|
||||||
|
get: function () {
|
||||||
|
return this.type;
|
||||||
|
},
|
||||||
|
set: function (value) {
|
||||||
|
if (this.type !== value) {
|
||||||
|
this.$emit('update:type', value);
|
||||||
|
|
||||||
|
if (value === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
||||||
|
this.frequencyValue = [this.firstDayOfWeek];
|
||||||
|
} else if (value === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
||||||
|
this.frequencyValue = [1];
|
||||||
|
} else {
|
||||||
|
this.frequencyValue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
frequencyValue: {
|
||||||
|
get: function () {
|
||||||
|
const values = this.modelValue.split(',');
|
||||||
|
const ret = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
if (values[i]) {
|
||||||
|
ret.push(parseInt(values[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortNumbersArray(ret);
|
||||||
|
},
|
||||||
|
set: function (value) {
|
||||||
|
this.$emit('update:modelValue', sortNumbersArray(value).join(','));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
displayFrequency() {
|
||||||
|
if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Disabled.type) {
|
||||||
|
return this.$t('Disabled');
|
||||||
|
} else if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
||||||
|
if (this.frequencyValue.length) {
|
||||||
|
return this.$t('format.misc.everyMultiDaysOfWeek', {
|
||||||
|
days: this.$locale.getMultiWeekdayLongNames(this.frequencyValue)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.$t('Weekly');
|
||||||
|
}
|
||||||
|
} else if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
||||||
|
if (this.frequencyValue.length) {
|
||||||
|
return this.$t('format.misc.everyMultiDaysOfMonth', {
|
||||||
|
days: this.$locale.getMultiMonthdayShortNames(this.frequencyValue)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.$t('Monthly');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onMenuStateChanged(state) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
self.$nextTick(() => {
|
||||||
|
if (self.$refs.dropdownMenu && self.$refs.dropdownMenu.parentElement) {
|
||||||
|
scrollToSelectedItem(self.$refs.dropdownMenu.parentElement, '.schedule-frequency-value-container', '.frequency-value-selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFrequencyValueSelected(value) {
|
||||||
|
for (let i = 0; i < this.frequencyValue.length; i++) {
|
||||||
|
if (this.frequencyValue[i] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schedule-frequency-select-menu {
|
||||||
|
max-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-frequency-select-menu .schedule-frequency-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-frequency-select-menu .schedule-frequency-type-container,
|
||||||
|
.schedule-frequency-select-menu .schedule-frequency-value-container {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 310px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
|
||||||
|
style="height: auto" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
|
||||||
|
<f7-toolbar>
|
||||||
|
<div class="swipe-handler"></div>
|
||||||
|
<div class="left">
|
||||||
|
<f7-link sheet-close :text="$t('Cancel')"></f7-link>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<f7-link :text="$t('Done')" @click="save"></f7-link>
|
||||||
|
</div>
|
||||||
|
</f7-toolbar>
|
||||||
|
<f7-page-content>
|
||||||
|
<div class="grid grid-cols-2 grid-gap">
|
||||||
|
<div>
|
||||||
|
<div class="schedule-frequency-type-container">
|
||||||
|
<f7-list dividers class="schedule-frequency-type-list no-margin-vertical">
|
||||||
|
<f7-list-item link="#" no-chevron
|
||||||
|
:key="type.type"
|
||||||
|
:title="type.displayName"
|
||||||
|
v-for="type in allTransactionScheduledFrequencyTypes"
|
||||||
|
@click="changeFrequencyType(type.type)">
|
||||||
|
<template #after>
|
||||||
|
<f7-icon class="list-item-showing" f7="chevron_right" v-if="currentFrequencyType === type.type"></f7-icon>
|
||||||
|
</template>
|
||||||
|
</f7-list-item>
|
||||||
|
</f7-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="schedule-frequency-value-container">
|
||||||
|
<f7-list dividers class="schedule-frequency-value-list no-margin-vertical"
|
||||||
|
v-if="currentFrequencyType === allTemplateScheduledFrequencyTypes.Disabled.type">
|
||||||
|
<f7-list-item :title="$t('None')"></f7-list-item>
|
||||||
|
</f7-list>
|
||||||
|
<f7-list dividers class="schedule-frequency-value-list no-margin-vertical"
|
||||||
|
v-if="currentFrequencyType === allTemplateScheduledFrequencyTypes.Weekly.type">
|
||||||
|
<f7-list-item checkbox
|
||||||
|
:class="isChecked(weekDay.type) ? 'list-item-selected' : ''"
|
||||||
|
:key="weekDay.type"
|
||||||
|
:value="weekDay.type"
|
||||||
|
:checked="isChecked(weekDay.type)"
|
||||||
|
:title="weekDay.displayName"
|
||||||
|
v-for="weekDay in allWeekDays"
|
||||||
|
@change="changeFrequencyValue">
|
||||||
|
</f7-list-item>
|
||||||
|
</f7-list>
|
||||||
|
<f7-list dividers class="schedule-frequency-value-list no-margin-vertical"
|
||||||
|
v-if="currentFrequencyType === allTemplateScheduledFrequencyTypes.Monthly.type">
|
||||||
|
<f7-list-item checkbox
|
||||||
|
:class="isChecked(monthDay.day) ? 'list-item-selected' : ''"
|
||||||
|
:key="monthDay.day"
|
||||||
|
:value="monthDay.day"
|
||||||
|
:checked="isChecked(monthDay.day)"
|
||||||
|
:title="monthDay.displayName"
|
||||||
|
v-for="monthDay in allAvailableMonthDays"
|
||||||
|
@change="changeFrequencyValue">
|
||||||
|
</f7-list-item>
|
||||||
|
</f7-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</f7-page-content>
|
||||||
|
</f7-sheet>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
|
import { sortNumbersArray } from '@/lib/common.js';
|
||||||
|
import { scrollToSelectedItem } from '@/lib/ui.mobile.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'type',
|
||||||
|
'modelValue',
|
||||||
|
'disabled',
|
||||||
|
'readonly',
|
||||||
|
'label',
|
||||||
|
'show'
|
||||||
|
],
|
||||||
|
emits: [
|
||||||
|
'update:type',
|
||||||
|
'update:modelValue',
|
||||||
|
'update:show'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentFrequencyType: self.type,
|
||||||
|
currentFrequencyValue: self.getFrequencyValues(self.modelValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useUserStore),
|
||||||
|
allTransactionScheduledFrequencyTypes() {
|
||||||
|
return this.$locale.getAllTransactionScheduledFrequencyTypes();
|
||||||
|
},
|
||||||
|
allTemplateScheduledFrequencyTypes() {
|
||||||
|
return templateConstants.allTemplateScheduledFrequencyTypes;
|
||||||
|
},
|
||||||
|
allWeekDays() {
|
||||||
|
return this.$locale.getAllWeekDays(this.firstDayOfWeek);
|
||||||
|
},
|
||||||
|
allAvailableMonthDays() {
|
||||||
|
const allAvailableDays = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 28; i++) {
|
||||||
|
allAvailableDays.push({
|
||||||
|
day: i,
|
||||||
|
displayName: this.$locale.getMonthdayShortName(i),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAvailableDays;
|
||||||
|
},
|
||||||
|
firstDayOfWeek() {
|
||||||
|
return this.userStore.currentUserFirstDayOfWeek;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSheetOpen(event) {
|
||||||
|
this.currentFrequencyType = this.type;
|
||||||
|
this.currentFrequencyValue = this.getFrequencyValues(this.modelValue);
|
||||||
|
scrollToSelectedItem(event.$el, '.schedule-frequency-value-container', 'li.list-item-selected');
|
||||||
|
},
|
||||||
|
onSheetClosed() {
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
changeFrequencyType(value) {
|
||||||
|
if (this.currentFrequencyType !== value) {
|
||||||
|
this.currentFrequencyType = value;
|
||||||
|
|
||||||
|
if (value === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
||||||
|
this.currentFrequencyValue = [this.firstDayOfWeek];
|
||||||
|
} else if (value === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
||||||
|
this.currentFrequencyValue = [1];
|
||||||
|
} else {
|
||||||
|
this.currentFrequencyValue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeFrequencyValue(e) {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
|
for (let i = 0; i < this.currentFrequencyValue.length; i++) {
|
||||||
|
if (this.currentFrequencyValue[i] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentFrequencyValue.push(value);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < this.currentFrequencyValue.length; i++) {
|
||||||
|
if (this.currentFrequencyValue[i] === value) {
|
||||||
|
this.currentFrequencyValue.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.$emit('update:type', this.currentFrequencyType);
|
||||||
|
this.$emit('update:modelValue', sortNumbersArray(this.currentFrequencyValue).join(','));
|
||||||
|
this.$emit('update:show', false);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$emit('update:show', false);
|
||||||
|
},
|
||||||
|
isChecked(value) {
|
||||||
|
for (let i = 0; i < this.currentFrequencyValue.length; i++) {
|
||||||
|
if (this.currentFrequencyValue[i] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getFrequencyValues(value) {
|
||||||
|
const values = value.split(',');
|
||||||
|
const ret = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
if (values[i]) {
|
||||||
|
ret.push(parseInt(values[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortNumbersArray(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schedule-frequency-type-container, .schedule-frequency-value-container {
|
||||||
|
height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-frequency-type-list.list .item-inner {
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-frequency-value-list-list.list .item-content {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+18
-1
@@ -1,7 +1,24 @@
|
|||||||
const allTemplateTypes = {
|
const allTemplateTypes = {
|
||||||
Normal: 1
|
Normal: 1,
|
||||||
|
Schedule: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const allTemplateScheduledFrequencyTypes = {
|
||||||
|
Disabled: {
|
||||||
|
type: 0,
|
||||||
|
name: 'Disabled'
|
||||||
|
},
|
||||||
|
Weekly: {
|
||||||
|
type: 1,
|
||||||
|
name: 'Weekly'
|
||||||
|
},
|
||||||
|
Monthly: {
|
||||||
|
type: 2,
|
||||||
|
name: 'Monthly'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
allTemplateTypes: allTemplateTypes,
|
allTemplateTypes: allTemplateTypes,
|
||||||
|
allTemplateScheduledFrequencyTypes: allTemplateScheduledFrequencyTypes,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import DateTimeSelect from '@/components/desktop/DateTimeSelect.vue';
|
|||||||
import ColorSelect from '@/components/desktop/ColorSelect.vue';
|
import ColorSelect from '@/components/desktop/ColorSelect.vue';
|
||||||
import IconSelect from '@/components/desktop/IconSelect.vue';
|
import IconSelect from '@/components/desktop/IconSelect.vue';
|
||||||
import TwoColumnSelect from '@/components/desktop/TwoColumnSelect.vue';
|
import TwoColumnSelect from '@/components/desktop/TwoColumnSelect.vue';
|
||||||
|
import ScheduleFrequencySelect from '@/components/desktop/ScheduleFrequencySelect.vue';
|
||||||
import StepsBar from '@/components/desktop/StepsBar.vue';
|
import StepsBar from '@/components/desktop/StepsBar.vue';
|
||||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||||
@@ -454,6 +455,7 @@ app.component('DateTimeSelect', DateTimeSelect);
|
|||||||
app.component('ColorSelect', ColorSelect);
|
app.component('ColorSelect', ColorSelect);
|
||||||
app.component('IconSelect', IconSelect);
|
app.component('IconSelect', IconSelect);
|
||||||
app.component('TwoColumnSelect', TwoColumnSelect);
|
app.component('TwoColumnSelect', TwoColumnSelect);
|
||||||
|
app.component('ScheduleFrequencySelect', ScheduleFrequencySelect);
|
||||||
app.component('StepsBar', StepsBar);
|
app.component('StepsBar', StepsBar);
|
||||||
app.component('ConfirmDialog', ConfirmDialog);
|
app.component('ConfirmDialog', ConfirmDialog);
|
||||||
app.component('SnackBar', SnackBar);
|
app.component('SnackBar', SnackBar);
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ export function isObjectEmpty(obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortNumbersArray(array) {
|
||||||
|
return array.sort(function (num1, num2) {
|
||||||
|
return num1 - num2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjectOwnFieldCount(object) {
|
export function getObjectOwnFieldCount(object) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
+76
-3
@@ -9,6 +9,7 @@ import colorConstants from '@/consts/color.js';
|
|||||||
import accountConstants from '@/consts/account.js';
|
import accountConstants from '@/consts/account.js';
|
||||||
import categoryConstants from '@/consts/category.js';
|
import categoryConstants from '@/consts/category.js';
|
||||||
import transactionConstants from '@/consts/transaction.js';
|
import transactionConstants from '@/consts/transaction.js';
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
import statisticsConstants from '@/consts/statistics.js';
|
import statisticsConstants from '@/consts/statistics.js';
|
||||||
import apiConstants from '@/consts/api.js';
|
import apiConstants from '@/consts/api.js';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
isString,
|
isString,
|
||||||
isNumber,
|
isNumber,
|
||||||
isBoolean,
|
isBoolean,
|
||||||
|
getNameByKeyValue,
|
||||||
copyObjectTo,
|
copyObjectTo,
|
||||||
copyArrayTo
|
copyArrayTo
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
@@ -311,6 +313,16 @@ function getMonthLongName(monthName, translateFn) {
|
|||||||
return translateFn(`datetime.${monthName}.long`);
|
return translateFn(`datetime.${monthName}.long`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMonthdayOrdinal(monthDay, translateFn) {
|
||||||
|
return translateFn(`datetime.monthDayOrdinal.${monthDay}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthdayShortName(monthDay, translateFn) {
|
||||||
|
return translateFn('format.misc.monthDay', {
|
||||||
|
ordinal: getMonthdayOrdinal(monthDay, translateFn)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getWeekdayShortName(weekDayName, translateFn) {
|
function getWeekdayShortName(weekDayName, translateFn) {
|
||||||
return translateFn(`datetime.${weekDayName}.short`);
|
return translateFn(`datetime.${weekDayName}.short`);
|
||||||
}
|
}
|
||||||
@@ -319,6 +331,30 @@ function getWeekdayLongName(weekDayName, translateFn) {
|
|||||||
return translateFn(`datetime.${weekDayName}.long`);
|
return translateFn(`datetime.${weekDayName}.long`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMultiMonthdayShortNames(monthDays, translateFn) {
|
||||||
|
if (!monthDays) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthDays.length === 1) {
|
||||||
|
return translateFn('format.misc.monthDay', {
|
||||||
|
ordinal: getMonthdayOrdinal(monthDays[0], translateFn)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return translateFn('format.misc.monthDays', {
|
||||||
|
multiMonthDays: joinMultiText(monthDays.map(monthDay =>
|
||||||
|
translateFn('format.misc.eachMonthDayInMonthDays', {
|
||||||
|
ordinal: getMonthdayOrdinal(monthDay, translateFn)
|
||||||
|
})), translateFn)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMultiWeekdayLongNames(weekdayTypes, translateFn) {
|
||||||
|
const allWeekDays = getAllWeekDays(null, translateFn)
|
||||||
|
return joinMultiText(weekdayTypes.map(type => getNameByKeyValue(allWeekDays, type, 'type', 'displayName')), translateFn);
|
||||||
|
}
|
||||||
|
|
||||||
function getI18nLongDateFormat(translateFn, formatTypeValue) {
|
function getI18nLongDateFormat(translateFn, formatTypeValue) {
|
||||||
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
|
const defaultLongDateFormatTypeName = translateFn('default.longDateFormat');
|
||||||
return getDateTimeFormat(translateFn, datetimeConstants.allLongDateFormat, datetimeConstants.allLongDateFormatArray, 'format.longDate', defaultLongDateFormatTypeName, datetimeConstants.defaultLongDateFormat, formatTypeValue);
|
return getDateTimeFormat(translateFn, datetimeConstants.allLongDateFormat, datetimeConstants.allLongDateFormatArray, 'format.longDate', defaultLongDateFormatTypeName, datetimeConstants.defaultLongDateFormat, formatTypeValue);
|
||||||
@@ -534,10 +570,23 @@ function getAllCurrencies(translateFn) {
|
|||||||
return allCurrencies;
|
return allCurrencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllWeekDays(translateFn) {
|
function getAllWeekDays(firstDayOfWeek, translateFn) {
|
||||||
const allWeekDays = [];
|
const allWeekDays = [];
|
||||||
|
|
||||||
for (let i = 0; i < datetimeConstants.allWeekDaysArray.length; i++) {
|
if (!isNumber(firstDayOfWeek)) {
|
||||||
|
firstDayOfWeek = datetimeConstants.allWeekDays.Sunday.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = firstDayOfWeek; i < datetimeConstants.allWeekDaysArray.length; i++) {
|
||||||
|
const weekDay = datetimeConstants.allWeekDaysArray[i];
|
||||||
|
|
||||||
|
allWeekDays.push({
|
||||||
|
type: weekDay.type,
|
||||||
|
displayName: translateFn(`datetime.${weekDay.name}.long`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < firstDayOfWeek; i++) {
|
||||||
const weekDay = datetimeConstants.allWeekDaysArray[i];
|
const weekDay = datetimeConstants.allWeekDaysArray[i];
|
||||||
|
|
||||||
allWeekDays.push({
|
allWeekDays.push({
|
||||||
@@ -1078,6 +1127,25 @@ function getAllTransactionEditScopeTypes(translateFn) {
|
|||||||
return allEditScopeTypes;
|
return allEditScopeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllTransactionScheduledFrequencyTypes(translateFn) {
|
||||||
|
const allScheduledFrequencyTypes = [];
|
||||||
|
|
||||||
|
for (const typeName in templateConstants.allTemplateScheduledFrequencyTypes) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(templateConstants.allTemplateScheduledFrequencyTypes, typeName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frequencyType = templateConstants.allTemplateScheduledFrequencyTypes[typeName];
|
||||||
|
|
||||||
|
allScheduledFrequencyTypes.push({
|
||||||
|
type: frequencyType.type,
|
||||||
|
displayName: translateFn(frequencyType.name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allScheduledFrequencyTypes;
|
||||||
|
}
|
||||||
|
|
||||||
function getAllTransactionDefaultCategories(categoryType, locale, translateFn) {
|
function getAllTransactionDefaultCategories(categoryType, locale, translateFn) {
|
||||||
const allCategories = {};
|
const allCategories = {};
|
||||||
const categoryTypes = [];
|
const categoryTypes = [];
|
||||||
@@ -1442,8 +1510,12 @@ export function i18nFunctions(i18nGlobal) {
|
|||||||
getAllShortTimeFormats: () => getAllShortTimeFormats(i18nGlobal.t),
|
getAllShortTimeFormats: () => getAllShortTimeFormats(i18nGlobal.t),
|
||||||
getMonthShortName: (month) => getMonthShortName(month, i18nGlobal.t),
|
getMonthShortName: (month) => getMonthShortName(month, i18nGlobal.t),
|
||||||
getMonthLongName: (month) => getMonthLongName(month, i18nGlobal.t),
|
getMonthLongName: (month) => getMonthLongName(month, i18nGlobal.t),
|
||||||
|
getMonthdayOrdinal: (monthDay) => getMonthdayOrdinal(monthDay, i18nGlobal.t),
|
||||||
|
getMonthdayShortName: (monthDay) => getMonthdayShortName(monthDay, i18nGlobal.t),
|
||||||
getWeekdayShortName: (weekDay) => getWeekdayShortName(weekDay, i18nGlobal.t),
|
getWeekdayShortName: (weekDay) => getWeekdayShortName(weekDay, i18nGlobal.t),
|
||||||
getWeekdayLongName: (weekDay) => getWeekdayLongName(weekDay, i18nGlobal.t),
|
getWeekdayLongName: (weekDay) => getWeekdayLongName(weekDay, i18nGlobal.t),
|
||||||
|
getMultiMonthdayShortNames: (monthdays) => getMultiMonthdayShortNames(monthdays, i18nGlobal.t),
|
||||||
|
getMultiWeekdayLongNames: (weekdayTypes) => getMultiWeekdayLongNames(weekdayTypes, i18nGlobal.t),
|
||||||
formatUnixTimeToLongDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat) + ' ' + getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset),
|
formatUnixTimeToLongDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat) + ' ' + getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset),
|
||||||
formatUnixTimeToShortDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortDateFormat(i18nGlobal.t, userStore.currentUserShortDateFormat) + ' ' + getI18nShortTimeFormat(i18nGlobal.t, userStore.currentUserShortTimeFormat), utcOffset, currentUtcOffset),
|
formatUnixTimeToShortDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortDateFormat(i18nGlobal.t, userStore.currentUserShortDateFormat) + ' ' + getI18nShortTimeFormat(i18nGlobal.t, userStore.currentUserShortTimeFormat), utcOffset, currentUtcOffset),
|
||||||
formatUnixTimeToLongDate: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset),
|
formatUnixTimeToLongDate: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset),
|
||||||
@@ -1465,7 +1537,7 @@ export function i18nFunctions(i18nGlobal) {
|
|||||||
getAllTimezones: (includeSystemDefault) => getAllTimezones(includeSystemDefault, i18nGlobal.t),
|
getAllTimezones: (includeSystemDefault) => getAllTimezones(includeSystemDefault, i18nGlobal.t),
|
||||||
getTimezoneDifferenceDisplayText: (utcOffset) => getTimezoneDifferenceDisplayText(utcOffset, i18nGlobal.t),
|
getTimezoneDifferenceDisplayText: (utcOffset) => getTimezoneDifferenceDisplayText(utcOffset, i18nGlobal.t),
|
||||||
getAllCurrencies: () => getAllCurrencies(i18nGlobal.t),
|
getAllCurrencies: () => getAllCurrencies(i18nGlobal.t),
|
||||||
getAllWeekDays: () => getAllWeekDays(i18nGlobal.t),
|
getAllWeekDays: (firstDayOfWeek) => getAllWeekDays(firstDayOfWeek, i18nGlobal.t),
|
||||||
getAllDateRanges: (scene, includeCustom) => getAllDateRanges(scene, includeCustom, i18nGlobal.t),
|
getAllDateRanges: (scene, includeCustom) => getAllDateRanges(scene, includeCustom, i18nGlobal.t),
|
||||||
getAllRecentMonthDateRanges: (userStore, includeAll, includeCustom) => getAllRecentMonthDateRanges(userStore, includeAll, includeCustom, i18nGlobal.t),
|
getAllRecentMonthDateRanges: (userStore, includeAll, includeCustom) => getAllRecentMonthDateRanges(userStore, includeAll, includeCustom, i18nGlobal.t),
|
||||||
getDateRangeDisplayName: (userStore, dateType, startTime, endTime) => getDateRangeDisplayName(userStore, dateType, startTime, endTime, i18nGlobal.t),
|
getDateRangeDisplayName: (userStore, dateType, startTime, endTime) => getDateRangeDisplayName(userStore, dateType, startTime, endTime, i18nGlobal.t),
|
||||||
@@ -1493,6 +1565,7 @@ export function i18nFunctions(i18nGlobal) {
|
|||||||
getAllStatisticsChartDataTypes: (analysisType) => getAllStatisticsChartDataTypes(i18nGlobal.t, analysisType),
|
getAllStatisticsChartDataTypes: (analysisType) => getAllStatisticsChartDataTypes(i18nGlobal.t, analysisType),
|
||||||
getAllStatisticsSortingTypes: () => getAllStatisticsSortingTypes(i18nGlobal.t),
|
getAllStatisticsSortingTypes: () => getAllStatisticsSortingTypes(i18nGlobal.t),
|
||||||
getAllTransactionEditScopeTypes: () => getAllTransactionEditScopeTypes(i18nGlobal.t),
|
getAllTransactionEditScopeTypes: () => getAllTransactionEditScopeTypes(i18nGlobal.t),
|
||||||
|
getAllTransactionScheduledFrequencyTypes: () => getAllTransactionScheduledFrequencyTypes(i18nGlobal.t),
|
||||||
getAllTransactionDefaultCategories: (categoryType, locale) => getAllTransactionDefaultCategories(categoryType, locale, i18nGlobal.t),
|
getAllTransactionDefaultCategories: (categoryType, locale) => getAllTransactionDefaultCategories(categoryType, locale, i18nGlobal.t),
|
||||||
getAllDisplayExchangeRates: (exchangeRatesData) => getAllDisplayExchangeRates(exchangeRatesData, i18nGlobal.t),
|
getAllDisplayExchangeRates: (exchangeRatesData) => getAllDisplayExchangeRates(exchangeRatesData, i18nGlobal.t),
|
||||||
getEnableDisableOptions: () => getEnableDisableOptions(i18nGlobal.t),
|
getEnableDisableOptions: () => getEnableDisableOptions(i18nGlobal.t),
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export function isUserVerifyEmailEnabled() {
|
|||||||
return getServerSetting('v') === '1';
|
return getServerSetting('v') === '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUserScheduledTransactionEnabled() {
|
||||||
|
return getServerSetting('s') === '1';
|
||||||
|
}
|
||||||
|
|
||||||
export function isDataExportingEnabled() {
|
export function isDataExportingEnabled() {
|
||||||
return getServerSetting('e') === '1';
|
return getServerSetting('e') === '1';
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -523,7 +523,7 @@ export default {
|
|||||||
getTransactionTemplate: ({ id }) => {
|
getTransactionTemplate: ({ id }) => {
|
||||||
return axios.get('v1/transaction/templates/get.json?id=' + id);
|
return axios.get('v1/transaction/templates/get.json?id=' + id);
|
||||||
},
|
},
|
||||||
addTransactionTemplate: ({ templateType, name, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, clientSessionId }) => {
|
addTransactionTemplate: ({ templateType, name, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, scheduledFrequencyType, scheduledFrequency, utcOffset, clientSessionId }) => {
|
||||||
return axios.post('v1/transaction/templates/add.json', {
|
return axios.post('v1/transaction/templates/add.json', {
|
||||||
templateType,
|
templateType,
|
||||||
name,
|
name,
|
||||||
@@ -536,10 +536,13 @@ export default {
|
|||||||
hideAmount,
|
hideAmount,
|
||||||
tagIds,
|
tagIds,
|
||||||
comment,
|
comment,
|
||||||
|
scheduledFrequencyType,
|
||||||
|
scheduledFrequency,
|
||||||
|
utcOffset,
|
||||||
clientSessionId
|
clientSessionId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
modifyTransactionTemplate: ({ id, name, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment }) => {
|
modifyTransactionTemplate: ({ id, name, type, categoryId, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, scheduledFrequencyType, scheduledFrequency, utcOffset }) => {
|
||||||
return axios.post('v1/transaction/templates/modify.json', {
|
return axios.post('v1/transaction/templates/modify.json', {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -551,7 +554,10 @@ export default {
|
|||||||
destinationAmount,
|
destinationAmount,
|
||||||
hideAmount,
|
hideAmount,
|
||||||
tagIds,
|
tagIds,
|
||||||
comment
|
comment,
|
||||||
|
scheduledFrequencyType,
|
||||||
|
scheduledFrequency,
|
||||||
|
utcOffset
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hideTransactionTemplate: ({ id, hidden }) => {
|
hideTransactionTemplate: ({ id, hidden }) => {
|
||||||
|
|||||||
@@ -73,6 +73,11 @@
|
|||||||
"hoursAheadOfDefaultTimezone": "{hours} hour(s) ahead of default timezone",
|
"hoursAheadOfDefaultTimezone": "{hours} hour(s) ahead of default timezone",
|
||||||
"hoursMinutesBehindDefaultTimezone": "{hours} hour(s) and {minutes} minutes behind default timezone",
|
"hoursMinutesBehindDefaultTimezone": "{hours} hour(s) and {minutes} minutes behind default timezone",
|
||||||
"hoursMinutesAheadOfDefaultTimezone": "{hours} hour(s) and {minutes} minutes ahead of default timezone",
|
"hoursMinutesAheadOfDefaultTimezone": "{hours} hour(s) and {minutes} minutes ahead of default timezone",
|
||||||
|
"monthDay": "{ordinal} day",
|
||||||
|
"eachMonthDayInMonthDays": "{ordinal}",
|
||||||
|
"monthDays": "{multiMonthDays} days",
|
||||||
|
"everyMultiDaysOfWeek": "Every {days}",
|
||||||
|
"everyMultiDaysOfMonth": "Every {days} of month",
|
||||||
"youHaveAccounts": "You have recorded {count} accounts",
|
"youHaveAccounts": "You have recorded {count} accounts",
|
||||||
"accountActivationAndResendValidationEmailTip": "Account activation link has been sent to your email address: {email}, If you don't receive the mail, please fill password again and click the button below to resend the validation mail.",
|
"accountActivationAndResendValidationEmailTip": "Account activation link has been sent to your email address: {email}, If you don't receive the mail, please fill password again and click the button below to resend the validation mail.",
|
||||||
"resendValidationEmailTip": "If you don't receive the mail, please fill password again and click the button below to resend the validation mail to: {email}"
|
"resendValidationEmailTip": "If you don't receive the mail, please fill password again and click the button below to resend the validation mail to: {email}"
|
||||||
@@ -171,6 +176,39 @@
|
|||||||
"December": {
|
"December": {
|
||||||
"short": "Dec",
|
"short": "Dec",
|
||||||
"long": "December"
|
"long": "December"
|
||||||
|
},
|
||||||
|
"monthDayOrdinal": {
|
||||||
|
"1": "1th",
|
||||||
|
"2": "2nd",
|
||||||
|
"3": "3rd",
|
||||||
|
"4": "4th",
|
||||||
|
"5": "5th",
|
||||||
|
"6": "6th",
|
||||||
|
"7": "7th",
|
||||||
|
"8": "8th",
|
||||||
|
"9": "9th",
|
||||||
|
"10": "10th",
|
||||||
|
"11": "11th",
|
||||||
|
"12": "12th",
|
||||||
|
"13": "13th",
|
||||||
|
"14": "14th",
|
||||||
|
"15": "15th",
|
||||||
|
"16": "16th",
|
||||||
|
"17": "17th",
|
||||||
|
"18": "18th",
|
||||||
|
"19": "19th",
|
||||||
|
"20": "20th",
|
||||||
|
"21": "21th",
|
||||||
|
"22": "22nd",
|
||||||
|
"23": "23rd",
|
||||||
|
"24": "24th",
|
||||||
|
"25": "25th",
|
||||||
|
"26": "26th",
|
||||||
|
"27": "27th",
|
||||||
|
"28": "28th",
|
||||||
|
"29": "29th",
|
||||||
|
"30": "30th",
|
||||||
|
"31": "31th"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"numeral": {
|
"numeral": {
|
||||||
@@ -1047,6 +1085,8 @@
|
|||||||
"transaction template id is invalid": "Transaction template ID is invalid",
|
"transaction template id is invalid": "Transaction template ID is invalid",
|
||||||
"transaction template not found": "Transaction template is not found",
|
"transaction template not found": "Transaction template is not found",
|
||||||
"transaction template type is invalid": "Transaction template type is invalid",
|
"transaction template type is invalid": "Transaction template type is invalid",
|
||||||
|
"scheduled transaction is not enabled": "Scheduled transaction is not enabled",
|
||||||
|
"scheduled transaction frequency is invalid": "Scheduled transaction frequency is invalid",
|
||||||
"query items cannot be blank": "There are no query items",
|
"query items cannot be blank": "There are no query items",
|
||||||
"query items too much": "There are too many query items",
|
"query items too much": "There are too many query items",
|
||||||
"query items have invalid item": "There is invalid item in query items",
|
"query items have invalid item": "There is invalid item in query items",
|
||||||
@@ -1168,6 +1208,8 @@
|
|||||||
"Select Date": "Select Date",
|
"Select Date": "Select Date",
|
||||||
"Select Time": "Select Time",
|
"Select Time": "Select Time",
|
||||||
"Now": "Now",
|
"Now": "Now",
|
||||||
|
"Weekly": "Weekly",
|
||||||
|
"Monthly": "Monthly",
|
||||||
"Custom": "Custom",
|
"Custom": "Custom",
|
||||||
"Greater than": "Greater than",
|
"Greater than": "Greater than",
|
||||||
"Less than": "Less than",
|
"Less than": "Less than",
|
||||||
@@ -1368,6 +1410,8 @@
|
|||||||
"Edit Transaction": "Edit Transaction",
|
"Edit Transaction": "Edit Transaction",
|
||||||
"Add Transaction Template": "Add Transaction Template",
|
"Add Transaction Template": "Add Transaction Template",
|
||||||
"Edit Transaction Template": "Edit Transaction Template",
|
"Edit Transaction Template": "Edit Transaction Template",
|
||||||
|
"Add Scheduled Transaction": "Add Scheduled Transaction",
|
||||||
|
"Edit Scheduled Transaction": "Edit Scheduled Transaction",
|
||||||
"Modify Balance": "Modify Balance",
|
"Modify Balance": "Modify Balance",
|
||||||
"Expense Amount": "Expense Amount",
|
"Expense Amount": "Expense Amount",
|
||||||
"Income Amount": "Income Amount",
|
"Income Amount": "Income Amount",
|
||||||
@@ -1387,6 +1431,7 @@
|
|||||||
"Without Tags": "Without Tags",
|
"Without Tags": "Without Tags",
|
||||||
"Multiple Tags": "Multiple Tags",
|
"Multiple Tags": "Multiple Tags",
|
||||||
"Transaction Time": "Transaction Time",
|
"Transaction Time": "Transaction Time",
|
||||||
|
"Scheduled Transaction Frequency": "Scheduled Transaction Frequency",
|
||||||
"Transaction Timezone": "Transaction Timezone",
|
"Transaction Timezone": "Transaction Timezone",
|
||||||
"Same time as default timezone": "Same time as default timezone",
|
"Same time as default timezone": "Same time as default timezone",
|
||||||
"Geographic Location": "Geographic Location",
|
"Geographic Location": "Geographic Location",
|
||||||
@@ -1622,10 +1667,12 @@
|
|||||||
"Show Hidden Transaction Tags": "Show Hidden Transaction Tags",
|
"Show Hidden Transaction Tags": "Show Hidden Transaction Tags",
|
||||||
"Hide Hidden Transaction Tags": "Hide Hidden Transaction Tags",
|
"Hide Hidden Transaction Tags": "Hide Hidden Transaction Tags",
|
||||||
"Transaction Templates": "Transaction Templates",
|
"Transaction Templates": "Transaction Templates",
|
||||||
|
"Scheduled Transactions": "Scheduled Transactions",
|
||||||
"Template Name": "Template Name",
|
"Template Name": "Template Name",
|
||||||
"No available template": "No available template",
|
"No available template": "No available template",
|
||||||
"Once you add templates, you can long press the Add button on the home page to quickly add a new transaction": "Once you add templates, you can long press the Add button on the home page to quickly add a new transaction",
|
"Once you add templates, you can long press the Add button on the home page to quickly add a new transaction": "Once you add templates, you can long press the Add button on the home page to quickly add a new transaction",
|
||||||
"No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page": "No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page",
|
"No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page": "No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page",
|
||||||
|
"No available scheduled transactions": "No available scheduled transactions",
|
||||||
"Unable to retrieve template list": "Unable to retrieve template list",
|
"Unable to retrieve template list": "Unable to retrieve template list",
|
||||||
"Template list is up to date": "Template list is up to date",
|
"Template list is up to date": "Template list is up to date",
|
||||||
"Template list has been updated": "Template list has been updated",
|
"Template list has been updated": "Template list has been updated",
|
||||||
|
|||||||
@@ -73,6 +73,11 @@
|
|||||||
"hoursAheadOfDefaultTimezone": "比默认时区早{hours}小时",
|
"hoursAheadOfDefaultTimezone": "比默认时区早{hours}小时",
|
||||||
"hoursMinutesBehindDefaultTimezone": "比默认时区晚{hours}小时{minutes}分",
|
"hoursMinutesBehindDefaultTimezone": "比默认时区晚{hours}小时{minutes}分",
|
||||||
"hoursMinutesAheadOfDefaultTimezone": "比默认时区早{time}小时{minutes}分",
|
"hoursMinutesAheadOfDefaultTimezone": "比默认时区早{time}小时{minutes}分",
|
||||||
|
"monthDay": "{ordinal}日",
|
||||||
|
"eachMonthDayInMonthDays": "{ordinal}日",
|
||||||
|
"monthDays": "{multiMonthDays}",
|
||||||
|
"everyMultiDaysOfWeek": "每{days}",
|
||||||
|
"everyMultiDaysOfMonth": "每月{days}",
|
||||||
"youHaveAccounts": "您已经记录了 {count} 个账户",
|
"youHaveAccounts": "您已经记录了 {count} 个账户",
|
||||||
"accountActivationAndResendValidationEmailTip": "账号激活链接已经发送到您的邮箱地址:{email},如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件。",
|
"accountActivationAndResendValidationEmailTip": "账号激活链接已经发送到您的邮箱地址:{email},如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件。",
|
||||||
"resendValidationEmailTip": "如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件到:{email}"
|
"resendValidationEmailTip": "如果您没有收到邮件,请再次输入密码并点击下方的按钮重新发送验证邮件到:{email}"
|
||||||
@@ -171,6 +176,39 @@
|
|||||||
"December": {
|
"December": {
|
||||||
"short": "12月",
|
"short": "12月",
|
||||||
"long": "十二月"
|
"long": "十二月"
|
||||||
|
},
|
||||||
|
"monthDayOrdinal": {
|
||||||
|
"1": "1",
|
||||||
|
"2": "2",
|
||||||
|
"3": "3",
|
||||||
|
"4": "4",
|
||||||
|
"5": "5",
|
||||||
|
"6": "6",
|
||||||
|
"7": "7",
|
||||||
|
"8": "8",
|
||||||
|
"9": "9",
|
||||||
|
"10": "10",
|
||||||
|
"11": "11",
|
||||||
|
"12": "12",
|
||||||
|
"13": "13",
|
||||||
|
"14": "14",
|
||||||
|
"15": "15",
|
||||||
|
"16": "16",
|
||||||
|
"17": "17",
|
||||||
|
"18": "18",
|
||||||
|
"19": "19",
|
||||||
|
"20": "20",
|
||||||
|
"21": "21",
|
||||||
|
"22": "22",
|
||||||
|
"23": "23",
|
||||||
|
"24": "24",
|
||||||
|
"25": "25",
|
||||||
|
"26": "26",
|
||||||
|
"27": "27",
|
||||||
|
"28": "28",
|
||||||
|
"29": "29",
|
||||||
|
"30": "30",
|
||||||
|
"31": "31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"numeral": {
|
"numeral": {
|
||||||
@@ -1047,6 +1085,8 @@
|
|||||||
"transaction template id is invalid": "交易模板ID无效",
|
"transaction template id is invalid": "交易模板ID无效",
|
||||||
"transaction template not found": "交易模板不存在",
|
"transaction template not found": "交易模板不存在",
|
||||||
"transaction template type is invalid": "交易模板类型无效",
|
"transaction template type is invalid": "交易模板类型无效",
|
||||||
|
"scheduled transaction is not enabled": "定时交易没有启用",
|
||||||
|
"scheduled transaction frequency is invalid": "定时交易周期无效",
|
||||||
"query items cannot be blank": "请求项目不能为空",
|
"query items cannot be blank": "请求项目不能为空",
|
||||||
"query items too much": "请求项目过多",
|
"query items too much": "请求项目过多",
|
||||||
"query items have invalid item": "请求项目中有非法项目",
|
"query items have invalid item": "请求项目中有非法项目",
|
||||||
@@ -1168,6 +1208,8 @@
|
|||||||
"Select Date": "选择日期",
|
"Select Date": "选择日期",
|
||||||
"Select Time": "选择时间",
|
"Select Time": "选择时间",
|
||||||
"Now": "现在",
|
"Now": "现在",
|
||||||
|
"Weekly": "每周",
|
||||||
|
"Monthly": "每月",
|
||||||
"Custom": "自定义",
|
"Custom": "自定义",
|
||||||
"Greater than": "大于",
|
"Greater than": "大于",
|
||||||
"Less than": "小于",
|
"Less than": "小于",
|
||||||
@@ -1368,6 +1410,8 @@
|
|||||||
"Edit Transaction": "编辑交易",
|
"Edit Transaction": "编辑交易",
|
||||||
"Add Transaction Template": "添加交易模板",
|
"Add Transaction Template": "添加交易模板",
|
||||||
"Edit Transaction Template": "编辑交易模板",
|
"Edit Transaction Template": "编辑交易模板",
|
||||||
|
"Add Scheduled Transaction": "添加定时交易",
|
||||||
|
"Edit Scheduled Transaction": "编辑定时交易",
|
||||||
"Modify Balance": "修改余额",
|
"Modify Balance": "修改余额",
|
||||||
"Expense Amount": "支出金额",
|
"Expense Amount": "支出金额",
|
||||||
"Income Amount": "收入金额",
|
"Income Amount": "收入金额",
|
||||||
@@ -1387,6 +1431,7 @@
|
|||||||
"Without Tags": "没有标签",
|
"Without Tags": "没有标签",
|
||||||
"Multiple Tags": "多个标签",
|
"Multiple Tags": "多个标签",
|
||||||
"Transaction Time": "交易时间",
|
"Transaction Time": "交易时间",
|
||||||
|
"Scheduled Transaction Frequency": "定时交易周期",
|
||||||
"Transaction Timezone": "交易时区",
|
"Transaction Timezone": "交易时区",
|
||||||
"Same time as default timezone": "与默认时区时间相同",
|
"Same time as default timezone": "与默认时区时间相同",
|
||||||
"Geographic Location": "地理位置",
|
"Geographic Location": "地理位置",
|
||||||
@@ -1622,10 +1667,12 @@
|
|||||||
"Show Hidden Transaction Tags": "显示隐藏的交易标签",
|
"Show Hidden Transaction Tags": "显示隐藏的交易标签",
|
||||||
"Hide Hidden Transaction Tags": "不显示隐藏的交易标签",
|
"Hide Hidden Transaction Tags": "不显示隐藏的交易标签",
|
||||||
"Transaction Templates": "交易模板",
|
"Transaction Templates": "交易模板",
|
||||||
|
"Scheduled Transactions": "定时交易",
|
||||||
"Template Name": "模板名称",
|
"Template Name": "模板名称",
|
||||||
"No available template": "没有可用的模板",
|
"No available template": "没有可用的模板",
|
||||||
"Once you add templates, you can long press the Add button on the home page to quickly add a new transaction": "当添加模板后,您可以在主界面长按添加按钮快速添加新的交易",
|
"Once you add templates, you can long press the Add button on the home page to quickly add a new transaction": "当添加模板后,您可以在主界面长按添加按钮快速添加新的交易",
|
||||||
"No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page": "没有可用的模板。当添加模板后,您可以通过交易列表添加按钮的下拉菜单快速添加新的交易",
|
"No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page": "没有可用的模板。当添加模板后,您可以通过交易列表添加按钮的下拉菜单快速添加新的交易",
|
||||||
|
"No available scheduled transactions": "没有可用的定时交易",
|
||||||
"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": "模板列表已更新",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ import InformationSheet from '@/components/mobile/InformationSheet.vue';
|
|||||||
import NumberPadSheet from '@/components/mobile/NumberPadSheet.vue';
|
import NumberPadSheet from '@/components/mobile/NumberPadSheet.vue';
|
||||||
import MapSheet from '@/components/mobile/MapSheet.vue';
|
import MapSheet from '@/components/mobile/MapSheet.vue';
|
||||||
import TransactionTagSelectionSheet from '@/components/mobile/TransactionTagSelectionSheet.vue';
|
import TransactionTagSelectionSheet from '@/components/mobile/TransactionTagSelectionSheet.vue';
|
||||||
|
import ScheduleFrequencySheet from '@/components/mobile/ScheduleFrequencySheet.vue';
|
||||||
|
|
||||||
import TextareaAutoSize from '@/directives/mobile/textareaAutoSize.js';
|
import TextareaAutoSize from '@/directives/mobile/textareaAutoSize.js';
|
||||||
|
|
||||||
@@ -188,6 +189,7 @@ app.component('InformationSheet', InformationSheet);
|
|||||||
app.component('NumberPadSheet', NumberPadSheet);
|
app.component('NumberPadSheet', NumberPadSheet);
|
||||||
app.component('MapSheet', MapSheet);
|
app.component('MapSheet', MapSheet);
|
||||||
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
|
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
|
||||||
|
app.component('ScheduleFrequencySheet', ScheduleFrequencySheet);
|
||||||
|
|
||||||
app.directive('TextareaAutoSize', TextareaAutoSize);
|
app.directive('TextareaAutoSize', TextareaAutoSize);
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
import userState from '@/lib/userstate.js';
|
import userState from '@/lib/userstate.js';
|
||||||
|
|
||||||
import MainLayout from '@/views/desktop/MainLayout.vue';
|
import MainLayout from '@/views/desktop/MainLayout.vue';
|
||||||
@@ -141,7 +142,18 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/template/list',
|
path: '/template/list',
|
||||||
component: TransactionTemplateListPage,
|
component: TransactionTemplateListPage,
|
||||||
beforeEnter: checkLogin
|
beforeEnter: checkLogin,
|
||||||
|
props: {
|
||||||
|
initType: templateConstants.allTemplateTypes.Normal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/schedule/list',
|
||||||
|
component: TransactionTemplateListPage,
|
||||||
|
beforeEnter: checkLogin,
|
||||||
|
props: {
|
||||||
|
initType: templateConstants.allTemplateTypes.Schedule
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/exchange_rates',
|
path: '/exchange_rates',
|
||||||
|
|||||||
@@ -297,6 +297,11 @@ const routes = [
|
|||||||
async: asyncResolve(TemplateListPage),
|
async: asyncResolve(TemplateListPage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/schedule/list',
|
||||||
|
async: asyncResolve(TemplateListPage),
|
||||||
|
beforeEnter: [checkLogin]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/template/add',
|
path: '/template/add',
|
||||||
async: asyncResolve(TransactionEditPage),
|
async: asyncResolve(TransactionEditPage),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import transactionConstants from '@/consts/transaction.js';
|
import transactionConstants from '@/consts/transaction.js';
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
import { isDefined, isObject, isArray, isEquals } from '@/lib/common.js';
|
import { isDefined, isObject, isArray, isEquals } from '@/lib/common.js';
|
||||||
import services from '@/lib/services.js';
|
import services from '@/lib/services.js';
|
||||||
import logger from '@/lib/logger.js';
|
import logger from '@/lib/logger.js';
|
||||||
@@ -230,6 +231,12 @@ export const useTransactionTemplatesStore = defineStore('transactionTemplates',
|
|||||||
submitTemplate.clientSessionId = clientSessionId;
|
submitTemplate.clientSessionId = clientSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (template.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
submitTemplate.scheduledFrequencyType = template.scheduledFrequencyType;
|
||||||
|
submitTemplate.scheduledFrequency = template.scheduledFrequency;
|
||||||
|
submitTemplate.utcOffset = template.utcOffset;
|
||||||
|
}
|
||||||
|
|
||||||
if (template.type === transactionConstants.allTransactionTypes.Expense) {
|
if (template.type === transactionConstants.allTransactionTypes.Expense) {
|
||||||
submitTemplate.categoryId = template.expenseCategory;
|
submitTemplate.categoryId = template.expenseCategory;
|
||||||
} else if (template.type === transactionConstants.allTransactionTypes.Income) {
|
} else if (template.type === transactionConstants.allTransactionTypes.Income) {
|
||||||
|
|||||||
@@ -67,6 +67,12 @@
|
|||||||
<span class="nav-item-title">{{ $t('Transaction Templates') }}</span>
|
<span class="nav-item-title">{{ $t('Transaction Templates') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-link" v-if="isUserScheduledTransactionEnabled">
|
||||||
|
<router-link to="/schedule/list">
|
||||||
|
<v-icon class="nav-item-icon" :icon="icons.scheduledTransactions"/>
|
||||||
|
<span class="nav-item-title">{{ $t('Scheduled Transactions') }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li class="nav-section-title">
|
<li class="nav-section-title">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<span class="title-text">{{ $t('Miscellaneous') }}</span>
|
<span class="title-text">{{ $t('Miscellaneous') }}</span>
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="layout-page-content">
|
<div class="layout-page-content">
|
||||||
<div class="page-content-container">
|
<div class="page-content-container">
|
||||||
<router-view/>
|
<router-view :key="currentRoutePath" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,6 +194,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { useDisplay } from 'vuetify';
|
import { useDisplay } from 'vuetify';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useRootStore } from '@/stores/index.js';
|
import { useRootStore } from '@/stores/index.js';
|
||||||
@@ -195,6 +202,7 @@ import { useSettingsStore } from '@/stores/setting.js';
|
|||||||
import { useUserStore } from '@/stores/user.js';
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
|
||||||
import assetConstants from '@/consts/asset.js';
|
import assetConstants from '@/consts/asset.js';
|
||||||
|
import { isUserScheduledTransactionEnabled } from '@/lib/server_settings.js';
|
||||||
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -205,6 +213,7 @@ import {
|
|||||||
mdiViewDashboardOutline,
|
mdiViewDashboardOutline,
|
||||||
mdiTagOutline,
|
mdiTagOutline,
|
||||||
mdiClipboardTextOutline,
|
mdiClipboardTextOutline,
|
||||||
|
mdiClipboardTextClockOutline,
|
||||||
mdiChartPieOutline,
|
mdiChartPieOutline,
|
||||||
mdiSwapHorizontal,
|
mdiSwapHorizontal,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
@@ -235,6 +244,7 @@ export default {
|
|||||||
categories: mdiViewDashboardOutline,
|
categories: mdiViewDashboardOutline,
|
||||||
tags: mdiTagOutline,
|
tags: mdiTagOutline,
|
||||||
templates: mdiClipboardTextOutline,
|
templates: mdiClipboardTextOutline,
|
||||||
|
scheduledTransactions: mdiClipboardTextClockOutline,
|
||||||
statistics: mdiChartPieOutline,
|
statistics: mdiChartPieOutline,
|
||||||
exchangeRates: mdiSwapHorizontal,
|
exchangeRates: mdiSwapHorizontal,
|
||||||
settings: mdiCogOutline,
|
settings: mdiCogOutline,
|
||||||
@@ -258,6 +268,10 @@ export default {
|
|||||||
mdAndDown() {
|
mdAndDown() {
|
||||||
return this.display.mdAndDown.value;
|
return this.display.mdAndDown.value;
|
||||||
},
|
},
|
||||||
|
currentRoutePath() {
|
||||||
|
const route = useRoute();
|
||||||
|
return route.path;
|
||||||
|
},
|
||||||
currentNickName() {
|
currentNickName() {
|
||||||
return this.userStore.currentUserNickname || this.$t('User');
|
return this.userStore.currentUserNickname || this.$t('User');
|
||||||
},
|
},
|
||||||
@@ -280,6 +294,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isUserScheduledTransactionEnabled() {
|
||||||
|
return isUserScheduledTransactionEnabled();
|
||||||
|
},
|
||||||
isEnableApplicationLock() {
|
isEnableApplicationLock() {
|
||||||
return this.settingsStore.appSettings.applicationLock;
|
return this.settingsStore.appSettings.applicationLock;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title-and-toolbar d-flex align-center">
|
<div class="title-and-toolbar d-flex align-center">
|
||||||
<span>{{ $t('Transaction Templates') }}</span>
|
<span>{{ templateType === allTemplateTypes.Schedule ? $t('Scheduled Transactions') : $t('Transaction Templates') }}</span>
|
||||||
<v-btn class="ml-3" color="default" variant="outlined"
|
<v-btn class="ml-3" color="default" variant="outlined"
|
||||||
:disabled="loading || updating" @click="add">{{ $t('Add') }}</v-btn>
|
:disabled="loading || updating" @click="add">{{ $t('Add') }}</v-btn>
|
||||||
<v-btn class="ml-3" color="primary" variant="tonal"
|
<v-btn class="ml-3" color="primary" variant="tonal"
|
||||||
@@ -60,7 +60,9 @@
|
|||||||
|
|
||||||
<tbody v-if="!loading && noAvailableTemplate">
|
<tbody v-if="!loading && noAvailableTemplate">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page') }}</td>
|
<td v-if="templateType === allTemplateTypes.Normal">{{ $t('No available template. Once you add templates, you can quickly add a new transaction using the dropdown menu of the Add button on the transaction list page') }}</td>
|
||||||
|
<td v-if="templateType === allTemplateTypes.Schedule">{{ $t('No available scheduled transactions') }}</td>
|
||||||
|
<td v-else>{{ $t('No available template') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
@@ -79,9 +81,9 @@
|
|||||||
<v-badge class="right-bottom-icon" color="secondary"
|
<v-badge class="right-bottom-icon" color="secondary"
|
||||||
location="bottom right" offset-x="8" :icon="icons.hide"
|
location="bottom right" offset-x="8" :icon="icons.hide"
|
||||||
v-if="element.hidden">
|
v-if="element.hidden">
|
||||||
<v-icon size="20" start :icon="icons.text"/>
|
<v-icon size="20" start :icon="templateType === allTemplateTypes.Schedule ? icons.clock : icons.text"/>
|
||||||
</v-badge>
|
</v-badge>
|
||||||
<v-icon size="20" start :icon="icons.text" v-else-if="!element.hidden"/>
|
<v-icon size="20" start :icon="templateType === allTemplateTypes.Schedule ? icons.clock : icons.text" v-else-if="!element.hidden"/>
|
||||||
<span class="transaction-template-name">{{ element.name }}</span>
|
<span class="transaction-template-name">{{ element.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,13 +164,17 @@ import {
|
|||||||
mdiDeleteOutline,
|
mdiDeleteOutline,
|
||||||
mdiDrag,
|
mdiDrag,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiTextBoxOutline
|
mdiTextBoxOutline,
|
||||||
|
mdiClockTimeNineOutline
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditDialog
|
EditDialog
|
||||||
},
|
},
|
||||||
|
props: [
|
||||||
|
'initType',
|
||||||
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
templateType: templateConstants.allTemplateTypes.Normal,
|
templateType: templateConstants.allTemplateTypes.Normal,
|
||||||
@@ -189,7 +195,8 @@ export default {
|
|||||||
remove: mdiDeleteOutline,
|
remove: mdiDeleteOutline,
|
||||||
drag: mdiDrag,
|
drag: mdiDrag,
|
||||||
more: mdiDotsVertical,
|
more: mdiDotsVertical,
|
||||||
text: mdiTextBoxOutline
|
text: mdiTextBoxOutline,
|
||||||
|
clock: mdiClockTimeNineOutline
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -217,11 +224,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
},
|
||||||
|
allTemplateTypes() {
|
||||||
|
return templateConstants.allTemplateTypes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
self.templateType = self.initType;
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
|
|
||||||
self.transactionTemplatesStore.loadAllTemplates({
|
self.transactionTemplatesStore.loadAllTemplates({
|
||||||
@@ -325,6 +336,7 @@ export default {
|
|||||||
self.$refs.editDialog.open({
|
self.$refs.editDialog.open({
|
||||||
id: template.id,
|
id: template.id,
|
||||||
currentTemplate: {
|
currentTemplate: {
|
||||||
|
templateType: template.templateType,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
type: template.type,
|
type: template.type,
|
||||||
categoryId: template.categoryId,
|
categoryId: template.categoryId,
|
||||||
@@ -334,7 +346,10 @@ export default {
|
|||||||
destinationAmount: template.destinationAmount,
|
destinationAmount: template.destinationAmount,
|
||||||
hideAmount: template.hideAmount,
|
hideAmount: template.hideAmount,
|
||||||
tagIds: template.tagIds,
|
tagIds: template.tagIds,
|
||||||
comment: template.comment
|
comment: template.comment,
|
||||||
|
scheduledFrequencyType: template.scheduledFrequencyType,
|
||||||
|
scheduledFrequency: template.scheduledFrequency,
|
||||||
|
utcOffset: template.utcOffset
|
||||||
}
|
}
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
if (result && result.message) {
|
if (result && result.message) {
|
||||||
|
|||||||
@@ -195,7 +195,15 @@
|
|||||||
v-model="transaction.time"
|
v-model="transaction.time"
|
||||||
@error="showDateTimeError" />
|
@error="showDateTimeError" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6" v-if="type === 'transaction'">
|
<v-col cols="12" md="6" v-if="type === 'template' && transaction.templateType === allTemplateTypes.Schedule">
|
||||||
|
<schedule-frequency-select
|
||||||
|
:readonly="mode === 'view'"
|
||||||
|
:disabled="loading || submitting"
|
||||||
|
:label="$t('Scheduled Transaction Frequency')"
|
||||||
|
v-model:type="transaction.scheduledFrequencyType"
|
||||||
|
v-model="transaction.scheduledFrequency" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6" v-if="type === 'transaction' || (type === 'template' && transaction.templateType === allTemplateTypes.Schedule)">
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
class="transaction-edit-timezone"
|
class="transaction-edit-timezone"
|
||||||
item-title="displayNameWithUtcOffset"
|
item-title="displayNameWithUtcOffset"
|
||||||
@@ -349,6 +357,7 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
|||||||
|
|
||||||
import categoryConstants from '@/consts/category.js';
|
import categoryConstants from '@/consts/category.js';
|
||||||
import transactionConstants from '@/consts/transaction.js';
|
import transactionConstants from '@/consts/transaction.js';
|
||||||
|
import templateConstants from '@/consts/template.js';
|
||||||
import logger from '@/lib/logger.js';
|
import logger from '@/lib/logger.js';
|
||||||
import {
|
import {
|
||||||
getNameByKeyValue
|
getNameByKeyValue
|
||||||
@@ -423,12 +432,18 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return 'Transaction Detail';
|
return 'Transaction Detail';
|
||||||
}
|
}
|
||||||
} else if (this.type === 'template') {
|
} else if (this.type === 'template' && this.transaction.templateType === templateConstants.allTemplateTypes.Normal) {
|
||||||
if (this.mode === 'add') {
|
if (this.mode === 'add') {
|
||||||
return 'Add Transaction Template';
|
return 'Add Transaction Template';
|
||||||
} else if (this.mode === 'edit') {
|
} else if (this.mode === 'edit') {
|
||||||
return 'Edit Transaction Template';
|
return 'Edit Transaction Template';
|
||||||
}
|
}
|
||||||
|
} else if (this.type === 'template' && this.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
if (this.mode === 'add') {
|
||||||
|
return 'Add Scheduled Transaction';
|
||||||
|
} else if (this.mode === 'edit') {
|
||||||
|
return 'Edit Scheduled Transaction';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@@ -497,6 +512,9 @@ export default {
|
|||||||
allCategoryTypes() {
|
allCategoryTypes() {
|
||||||
return categoryConstants.allCategoryTypes;
|
return categoryConstants.allCategoryTypes;
|
||||||
},
|
},
|
||||||
|
allTemplateTypes() {
|
||||||
|
return templateConstants.allTemplateTypes;
|
||||||
|
},
|
||||||
allTimezones() {
|
allTimezones() {
|
||||||
return this.$locale.getAllTimezones(true);
|
return this.$locale.getAllTimezones(true);
|
||||||
},
|
},
|
||||||
@@ -721,11 +739,22 @@ export default {
|
|||||||
self.transaction.templateType = options.templateType;
|
self.transaction.templateType = options.templateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
self.transaction.scheduledFrequencyType = templateConstants.allTemplateScheduledFrequencyTypes.Disabled.type;
|
||||||
|
self.transaction.scheduledFrequency = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (options && options.id) {
|
if (options && options.id) {
|
||||||
if (options.currentTemplate) {
|
if (options.currentTemplate) {
|
||||||
self.setTransaction(options.currentTemplate, options, false, false);
|
self.setTransaction(options.currentTemplate, options, false, false);
|
||||||
self.transaction.templateType = options.currentTemplate.templateType;
|
self.transaction.templateType = options.currentTemplate.templateType;
|
||||||
self.transaction.name = options.currentTemplate.name;
|
self.transaction.name = options.currentTemplate.name;
|
||||||
|
|
||||||
|
if (self.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
self.transaction.scheduledFrequencyType = options.currentTemplate.scheduledFrequencyType;
|
||||||
|
self.transaction.scheduledFrequency = options.currentTemplate.scheduledFrequency;
|
||||||
|
self.transaction.utcOffset = options.currentTemplate.utcOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mode = 'edit';
|
self.mode = 'edit';
|
||||||
@@ -772,6 +801,12 @@ export default {
|
|||||||
self.setTransaction(template, options, false, false);
|
self.setTransaction(template, options, false, false);
|
||||||
self.transaction.templateType = template.templateType;
|
self.transaction.templateType = template.templateType;
|
||||||
self.transaction.name = template.name;
|
self.transaction.name = template.name;
|
||||||
|
|
||||||
|
if (self.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
self.transaction.scheduledFrequencyType = template.scheduledFrequencyType;
|
||||||
|
self.transaction.scheduledFrequency = template.scheduledFrequency;
|
||||||
|
self.transaction.utcOffset = template.utcOffset;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.setTransaction(null, options, true, true);
|
self.setTransaction(null, options, true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
count: displayDataStatistics ? displayDataStatistics.totalTransactionTemplateCount : '-',
|
count: displayDataStatistics ? displayDataStatistics.totalTransactionTemplateCount : '-',
|
||||||
icon: icons.templates,
|
icon: icons.templates,
|
||||||
color: 'secondary-darken-1'
|
color: 'secondary-darken-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Scheduled Transactions',
|
||||||
|
count: displayDataStatistics ? displayDataStatistics.totalScheduledTransactionCount : '-',
|
||||||
|
icon: icons.scheduledTransactions,
|
||||||
|
color: 'success-darken-1'
|
||||||
}
|
}
|
||||||
]">
|
]">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
@@ -161,6 +167,7 @@ import {
|
|||||||
mdiViewDashboardOutline,
|
mdiViewDashboardOutline,
|
||||||
mdiTagOutline,
|
mdiTagOutline,
|
||||||
mdiClipboardTextOutline,
|
mdiClipboardTextOutline,
|
||||||
|
mdiClipboardTextClockOutline,
|
||||||
mdiAlert
|
mdiAlert
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
@@ -179,6 +186,7 @@ export default {
|
|||||||
categories: mdiViewDashboardOutline,
|
categories: mdiViewDashboardOutline,
|
||||||
tags: mdiTagOutline,
|
tags: mdiTagOutline,
|
||||||
templates: mdiClipboardTextOutline,
|
templates: mdiClipboardTextOutline,
|
||||||
|
scheduledTransactions: mdiClipboardTextClockOutline,
|
||||||
alert: mdiAlert
|
alert: mdiAlert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +205,8 @@ export default {
|
|||||||
totalAccountCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalAccountCount),
|
totalAccountCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalAccountCount),
|
||||||
totalTransactionCategoryCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionCategoryCount),
|
totalTransactionCategoryCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionCategoryCount),
|
||||||
totalTransactionTagCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTagCount),
|
totalTransactionTagCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTagCount),
|
||||||
totalTransactionTemplateCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTemplateCount)
|
totalTransactionTemplateCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTemplateCount),
|
||||||
|
totalScheduledTransactionCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalScheduledTransactionCount)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isDataExportingEnabled() {
|
isDataExportingEnabled() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Transaction Templates')" link="/template/list"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Templates')" link="/template/list"></f7-list-item>
|
||||||
|
<f7-list-item :title="$t('Scheduled Transactions')" link="/schedule/list"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item>
|
<f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item>
|
<f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item>
|
<f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<f7-page :ptr="!sortable" @ptr:refresh="reload" @page:afterin="onPageAfterIn">
|
<f7-page :ptr="!sortable" @ptr:refresh="reload" @page:afterin="onPageAfterIn">
|
||||||
<f7-navbar>
|
<f7-navbar>
|
||||||
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
|
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
|
||||||
<f7-nav-title :title="$t('Transaction Templates')"></f7-nav-title>
|
<f7-nav-title :title="templateType === allTemplateTypes.Schedule ? $t('Scheduled Transactions') : $t('Transaction Templates')"></f7-nav-title>
|
||||||
<f7-nav-right class="navbar-compact-icons">
|
<f7-nav-right class="navbar-compact-icons">
|
||||||
<f7-link icon-f7="ellipsis" :class="{ 'disabled': !templates.length }" v-if="!sortable" @click="showMoreActionSheet = true"></f7-link>
|
<f7-link icon-f7="ellipsis" :class="{ 'disabled': !templates.length }" v-if="!sortable" @click="showMoreActionSheet = true"></f7-link>
|
||||||
<f7-link :href="'/template/add?templateType=' + templateType" icon-f7="plus" v-if="!sortable"></f7-link>
|
<f7-link :href="'/template/add?templateType=' + templateType" icon-f7="plus" v-if="!sortable"></f7-link>
|
||||||
@@ -21,7 +21,10 @@
|
|||||||
|
|
||||||
<f7-list strong inset dividers class="margin-top" v-if="!loading && noAvailableTemplate">
|
<f7-list strong inset dividers class="margin-top" v-if="!loading && noAvailableTemplate">
|
||||||
<f7-list-item :title="$t('No available template')"
|
<f7-list-item :title="$t('No available template')"
|
||||||
:footer="$t('Once you add templates, you can long press the Add button on the home page to quickly add a new transaction')"></f7-list-item>
|
:footer="$t('Once you add templates, you can long press the Add button on the home page to quickly add a new transaction')"
|
||||||
|
v-if="templateType === allTemplateTypes.Normal"></f7-list-item>
|
||||||
|
<f7-list-item :title="$t('No available scheduled transactions')" v-if="templateType === allTemplateTypes.Schedule"></f7-list-item>
|
||||||
|
<f7-list-item :title="$t('No available template')" v-else></f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
|
|
||||||
<f7-list strong inset dividers sortable class="margin-top template-list"
|
<f7-list strong inset dividers sortable class="margin-top template-list"
|
||||||
@@ -37,7 +40,7 @@
|
|||||||
v-show="showHidden || !template.hidden"
|
v-show="showHidden || !template.hidden"
|
||||||
@taphold="setSortable()">
|
@taphold="setSortable()">
|
||||||
<template #media>
|
<template #media>
|
||||||
<f7-icon f7="doc_plaintext">
|
<f7-icon :f7="templateType === allTemplateTypes.Schedule ? 'clock' : 'doc_plaintext'">
|
||||||
<f7-badge color="gray" class="right-bottom-icon" v-if="template.hidden">
|
<f7-badge color="gray" class="right-bottom-icon" v-if="template.hidden">
|
||||||
<f7-icon f7="eye_slash_fill"></f7-icon>
|
<f7-icon f7="eye_slash_fill"></f7-icon>
|
||||||
</f7-badge>
|
</f7-badge>
|
||||||
@@ -91,6 +94,7 @@ import { onSwipeoutDeleted } from '@/lib/ui.mobile.js';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
|
'f7route',
|
||||||
'f7router'
|
'f7router'
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
@@ -138,11 +142,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
allTemplateTypes() {
|
||||||
|
return templateConstants.allTemplateTypes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
if (self.f7route.path === '/template/list') {
|
||||||
|
self.templateType = templateConstants.allTemplateTypes.Normal;
|
||||||
|
} else if (self.f7route.path === '/schedule/list') {
|
||||||
|
self.templateType = templateConstants.allTemplateTypes.Schedule;
|
||||||
|
}
|
||||||
|
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
|
|
||||||
self.transactionTemplatesStore.loadAllTemplates({
|
self.transactionTemplatesStore.loadAllTemplates({
|
||||||
@@ -263,7 +276,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
edit(template) {
|
edit(template) {
|
||||||
this.f7router.navigate('/template/edit?id=' + template.id);
|
this.f7router.navigate(`/template/edit?id=${template.id}&templateType=${template.templateType}`);
|
||||||
},
|
},
|
||||||
hide(template, hidden) {
|
hide(template, hidden) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names"></f7-list-item>
|
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names"></f7-list-item>
|
||||||
<f7-list-item class="list-item-with-header-and-title" header="Account" title="Account Name"></f7-list-item>
|
<f7-list-item class="list-item-with-header-and-title" header="Account" title="Account Name"></f7-list-item>
|
||||||
<f7-list-item class="list-item-with-header-and-title" header="Transaction Time" title="YYYY/MM/DD HH:mm:ss" v-if="type === 'transaction'"></f7-list-item>
|
<f7-list-item class="list-item-with-header-and-title" header="Transaction Time" title="YYYY/MM/DD HH:mm:ss" v-if="type === 'transaction'"></f7-list-item>
|
||||||
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after" header="Transaction Timezone" title="(UTC XX:XX) System Default" link="#" :no-chevron="mode === 'view'" v-if="type === 'transaction'"></f7-list-item>
|
<f7-list-item class="list-item-with-header-and-title" header="Scheduled Transaction Frequency" title="Every XXXXX" v-if="type === 'template' && transaction.templateType === allTemplateTypes.Schedule"></f7-list-item>
|
||||||
|
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after" header="Transaction Timezone" title="(UTC XX:XX) System Default" link="#" :no-chevron="mode === 'view'" v-if="type === 'transaction' || (type === 'template' && transaction.templateType === allTemplateTypes.Schedule)"></f7-list-item>
|
||||||
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Geographic Location" title="No Location" v-if="type === 'transaction'"></f7-list-item>
|
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Geographic Location" title="No Location" v-if="type === 'transaction'"></f7-list-item>
|
||||||
<f7-list-item header="Tags">
|
<f7-list-item header="Tags">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -242,13 +243,28 @@
|
|||||||
</date-time-selection-sheet>
|
</date-time-selection-sheet>
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
|
|
||||||
|
<f7-list-item
|
||||||
|
class="list-item-with-header-and-title"
|
||||||
|
link="#" no-chevron
|
||||||
|
:class="{ 'readonly': mode === 'view' }"
|
||||||
|
:header="$t('Scheduled Transaction Frequency')"
|
||||||
|
:title="transactionDisplayScheduledFrequency"
|
||||||
|
@click="showTransactionScheduledFrequencySheet = true"
|
||||||
|
v-if="type === 'template' && transaction.templateType === allTemplateTypes.Schedule"
|
||||||
|
>
|
||||||
|
<schedule-frequency-sheet v-model:show="showTransactionScheduledFrequencySheet"
|
||||||
|
v-model:type="transaction.scheduledFrequencyType"
|
||||||
|
v-model="transaction.scheduledFrequency">
|
||||||
|
</schedule-frequency-sheet>
|
||||||
|
</f7-list-item>
|
||||||
|
|
||||||
<f7-list-item
|
<f7-list-item
|
||||||
:no-chevron="mode === 'view'"
|
:no-chevron="mode === 'view'"
|
||||||
class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after"
|
class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after"
|
||||||
:class="{ 'readonly': mode === 'view' }"
|
:class="{ 'readonly': mode === 'view' }"
|
||||||
:header="$t('Transaction Timezone')"
|
:header="$t('Transaction Timezone')"
|
||||||
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Transaction Timezone'), popupCloseLinkText: $t('Done') }"
|
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Transaction Timezone'), popupCloseLinkText: $t('Done') }"
|
||||||
v-if="type === 'transaction'"
|
v-if="type === 'transaction' || (type === 'template' && transaction.templateType === allTemplateTypes.Schedule)"
|
||||||
>
|
>
|
||||||
<select v-model="transaction.timeZone">
|
<select v-model="transaction.timeZone">
|
||||||
<option :value="timezone.name" :key="timezone.name"
|
<option :value="timezone.name" :key="timezone.name"
|
||||||
@@ -424,6 +440,7 @@ export default {
|
|||||||
showSourceAccountSheet: false,
|
showSourceAccountSheet: false,
|
||||||
showDestinationAccountSheet: false,
|
showDestinationAccountSheet: false,
|
||||||
showTransactionDateTimeSheet: false,
|
showTransactionDateTimeSheet: false,
|
||||||
|
showTransactionScheduledFrequencySheet: false,
|
||||||
showGeoLocationMapSheet: false,
|
showGeoLocationMapSheet: false,
|
||||||
showTransactionTagSheet: false
|
showTransactionTagSheet: false
|
||||||
};
|
};
|
||||||
@@ -439,12 +456,18 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return 'Transaction Detail';
|
return 'Transaction Detail';
|
||||||
}
|
}
|
||||||
} else if (this.type === 'template') {
|
} else if (this.type === 'template' && this.transaction.templateType === templateConstants.allTemplateTypes.Normal) {
|
||||||
if (this.mode === 'add') {
|
if (this.mode === 'add') {
|
||||||
return 'Add Transaction Template';
|
return 'Add Transaction Template';
|
||||||
} else if (this.mode === 'edit') {
|
} else if (this.mode === 'edit') {
|
||||||
return 'Edit Transaction Template';
|
return 'Edit Transaction Template';
|
||||||
}
|
}
|
||||||
|
} else if (this.type === 'template' && this.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
if (this.mode === 'add') {
|
||||||
|
return 'Add Scheduled Transaction';
|
||||||
|
} else if (this.mode === 'edit') {
|
||||||
|
return 'Edit Scheduled Transaction';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@@ -509,6 +532,9 @@ export default {
|
|||||||
allCategoryTypes() {
|
allCategoryTypes() {
|
||||||
return categoryConstants.allCategoryTypes;
|
return categoryConstants.allCategoryTypes;
|
||||||
},
|
},
|
||||||
|
allTemplateTypes() {
|
||||||
|
return templateConstants.allTemplateTypes;
|
||||||
|
},
|
||||||
allTimezones() {
|
allTimezones() {
|
||||||
return this.$locale.getAllTimezones(true);
|
return this.$locale.getAllTimezones(true);
|
||||||
},
|
},
|
||||||
@@ -581,6 +607,44 @@ export default {
|
|||||||
|
|
||||||
return `${this.$locale.formatUnixTimeToLongDateTime(this.userStore, getActualUnixTimeForStore(this.transaction.time, this.transaction.utcOffset, getBrowserTimezoneOffsetMinutes()))} (UTC${getTimezoneOffset(this.settingsStore.appSettings.timeZone)})`;
|
return `${this.$locale.formatUnixTimeToLongDateTime(this.userStore, getActualUnixTimeForStore(this.transaction.time, this.transaction.utcOffset, getBrowserTimezoneOffsetMinutes()))} (UTC${getTimezoneOffset(this.settingsStore.appSettings.timeZone)})`;
|
||||||
},
|
},
|
||||||
|
transactionDisplayScheduledFrequency() {
|
||||||
|
if (this.type !== 'template') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transaction.scheduledFrequencyType === templateConstants.allTemplateScheduledFrequencyTypes.Disabled.type) {
|
||||||
|
return this.$t('Disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = this.transaction.scheduledFrequency.split(',');
|
||||||
|
const scheduledFrequencyValues = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i]) {
|
||||||
|
scheduledFrequencyValues.push(parseInt(items[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.transaction.scheduledFrequencyType === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
||||||
|
if (scheduledFrequencyValues.length) {
|
||||||
|
return this.$t('format.misc.everyMultiDaysOfWeek', {
|
||||||
|
days: this.$locale.getMultiWeekdayLongNames(scheduledFrequencyValues)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.$t('Weekly');
|
||||||
|
}
|
||||||
|
} else if (this.transaction.scheduledFrequencyType === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
||||||
|
if (scheduledFrequencyValues.length) {
|
||||||
|
return this.$t('format.misc.everyMultiDaysOfMonth', {
|
||||||
|
days: this.$locale.getMultiMonthdayShortNames(scheduledFrequencyValues)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.$t('Monthly');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
transactionDisplayTimezone() {
|
transactionDisplayTimezone() {
|
||||||
return `UTC${getUtcOffsetByUtcOffsetMinutes(this.transaction.utcOffset)}`;
|
return `UTC${getUtcOffsetByUtcOffsetMinutes(this.transaction.utcOffset)}`;
|
||||||
},
|
},
|
||||||
@@ -750,6 +814,11 @@ export default {
|
|||||||
self.transaction.templateType = parseInt(query.templateType);
|
self.transaction.templateType = parseInt(query.templateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
self.transaction.scheduledFrequencyType = templateConstants.allTemplateScheduledFrequencyTypes.Disabled.type;
|
||||||
|
self.transaction.scheduledFrequency = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
if (self.mode === 'edit') {
|
if (self.mode === 'edit') {
|
||||||
self.editId = query.id;
|
self.editId = query.id;
|
||||||
@@ -820,6 +889,12 @@ export default {
|
|||||||
self.transaction.id = template.id;
|
self.transaction.id = template.id;
|
||||||
self.transaction.templateType = template.templateType;
|
self.transaction.templateType = template.templateType;
|
||||||
self.transaction.name = template.name;
|
self.transaction.name = template.name;
|
||||||
|
|
||||||
|
if (self.transaction.templateType === templateConstants.allTemplateTypes.Schedule) {
|
||||||
|
self.transaction.scheduledFrequencyType = template.scheduledFrequencyType;
|
||||||
|
self.transaction.scheduledFrequency = template.scheduledFrequency;
|
||||||
|
self.transaction.utcOffset = template.utcOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<f7-list-item title="Transaction Categories" after="Count"></f7-list-item>
|
<f7-list-item title="Transaction Categories" after="Count"></f7-list-item>
|
||||||
<f7-list-item title="Transaction Tags" after="Count"></f7-list-item>
|
<f7-list-item title="Transaction Tags" after="Count"></f7-list-item>
|
||||||
<f7-list-item title="Transaction Templates" after="Count"></f7-list-item>
|
<f7-list-item title="Transaction Templates" after="Count"></f7-list-item>
|
||||||
|
<f7-list-item title="Scheduled Transactions" after="Count"></f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
|
|
||||||
<f7-list strong inset dividers class="margin-vertical" v-else-if="!loading">
|
<f7-list strong inset dividers class="margin-vertical" v-else-if="!loading">
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
<f7-list-item :title="$t('Transaction Categories')" :after="displayDataStatistics.totalTransactionCategoryCount"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Categories')" :after="displayDataStatistics.totalTransactionCategoryCount"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Transaction Tags')" :after="displayDataStatistics.totalTransactionTagCount"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Tags')" :after="displayDataStatistics.totalTransactionTagCount"></f7-list-item>
|
||||||
<f7-list-item :title="$t('Transaction Templates')" :after="displayDataStatistics.totalTransactionTemplateCount"></f7-list-item>
|
<f7-list-item :title="$t('Transaction Templates')" :after="displayDataStatistics.totalTransactionTemplateCount"></f7-list-item>
|
||||||
|
<f7-list-item :title="$t('Scheduled Transactions')" :after="displayDataStatistics.totalScheduledTransactionCount"></f7-list-item>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
|
|
||||||
<f7-list strong inset dividers class="margin-vertical" :class="{ 'disabled': loading }">
|
<f7-list strong inset dividers class="margin-vertical" :class="{ 'disabled': loading }">
|
||||||
@@ -109,7 +111,8 @@ export default {
|
|||||||
totalAccountCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalAccountCount),
|
totalAccountCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalAccountCount),
|
||||||
totalTransactionCategoryCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionCategoryCount),
|
totalTransactionCategoryCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionCategoryCount),
|
||||||
totalTransactionTagCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTagCount),
|
totalTransactionTagCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTagCount),
|
||||||
totalTransactionTemplateCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTemplateCount)
|
totalTransactionTemplateCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalTransactionTemplateCount),
|
||||||
|
totalScheduledTransactionCount: self.$locale.appendDigitGroupingSymbol(self.userStore, self.dataStatistics.totalScheduledTransactionCount)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isDataExportingEnabled() {
|
isDataExportingEnabled() {
|
||||||
|
|||||||
Reference in New Issue
Block a user