diff --git a/cmd/user_data.go b/cmd/user_data.go index eba9b110..a88004d1 100644 --- a/cmd/user_data.go +++ b/cmd/user_data.go @@ -953,6 +953,7 @@ func printUserInfo(user *models.User) { fmt.Printf("[Password] %s\n", user.Password) fmt.Printf("[Salt] %s\n", user.Salt) fmt.Printf("[DefaultAccountId] %d\n", user.DefaultAccountId) + fmt.Printf("[UseLastReconciledTime] %t\n", user.UseLastReconciledTime) fmt.Printf("[TransactionEditScope] %s (%d)\n", user.TransactionEditScope, user.TransactionEditScope) fmt.Printf("[Language] %s\n", user.Language) fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency) diff --git a/pkg/api/accounts.go b/pkg/api/accounts.go index c0bb32b4..f396711f 100644 --- a/pkg/api/accounts.go +++ b/pkg/api/accounts.go @@ -766,6 +766,7 @@ func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.AccountModifyRequest, oldAccount *models.Account, isSubAccount bool) *models.Account { newAccountExtend := &models.AccountExtend{} + newAccountExtend.LastReconciledTime = accountModifyReq.LastReconciledTime if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD { newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate @@ -793,14 +794,17 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc return newAccount } - if (newAccount.Extend != nil && oldAccount.Extend == nil) || - (newAccount.Extend == nil && oldAccount.Extend != nil) { + oldAccountExtend := oldAccount.Extend + + if (newAccountExtend.LastReconciledTime != nil && (oldAccountExtend == nil || oldAccountExtend.LastReconciledTime == nil)) || + (newAccountExtend.LastReconciledTime == nil && oldAccountExtend != nil && oldAccountExtend.LastReconciledTime != nil) || + (newAccountExtend.LastReconciledTime != nil && oldAccountExtend != nil && oldAccountExtend.LastReconciledTime != nil && *newAccountExtend.LastReconciledTime != *oldAccountExtend.LastReconciledTime) { return newAccount } - oldAccountExtend := oldAccount.Extend - - if newAccountExtend.CreditCardStatementDate != oldAccountExtend.CreditCardStatementDate { + if (newAccountExtend.CreditCardStatementDate != nil && (oldAccountExtend == nil || oldAccountExtend.CreditCardStatementDate == nil)) || + (newAccountExtend.CreditCardStatementDate == nil && oldAccountExtend != nil && oldAccountExtend.CreditCardStatementDate != nil) || + (newAccountExtend.CreditCardStatementDate != nil && oldAccountExtend != nil && oldAccountExtend.CreditCardStatementDate != nil && *newAccountExtend.CreditCardStatementDate != *oldAccountExtend.CreditCardStatementDate) { return newAccount } diff --git a/pkg/api/forget_passwords.go b/pkg/api/forget_passwords.go index d9a648f1..d88e5b72 100644 --- a/pkg/api/forget_passwords.go +++ b/pkg/api/forget_passwords.go @@ -166,7 +166,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any, Password: request.Password, } - _, _, err = a.users.UpdateUser(c, userNew, false) + _, _, err = a.users.UpdateUser(c, userNew, false, false) if err != nil { log.Errorf(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error()) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index aea49cc0..b0cb6697 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -1075,7 +1075,15 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er } transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP()) - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, []*models.Transaction{transaction}) + + if err != nil { + log.Errorf(c, "[transactions.TransactionCreateHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime @@ -1268,8 +1276,15 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er return nil, errs.ErrNothingWillBeUpdated } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) - newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, clientTimezone) + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, []*models.Transaction{transaction, newTransaction}) + + if err != nil { + log.Errorf(c, "[transactions.TransactionModifyHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) + newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, clientTimezone, allUsedAccounts[newTransaction.AccountId], allUsedAccounts[newTransaction.RelatedAccountId]) if !transactionEditable || !newTransactionEditable { return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime @@ -1402,6 +1417,13 @@ func (a *TransactionsApi) TransactionBatchUpdateCategoriesHandler(c *core.WebCon return nil, errs.Or(err, errs.ErrOperationFailed) } + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, transactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + allTransactionIds := make([]int64, 0, len(transactions)) for i := 0; i < len(transactions); i++ { @@ -1412,7 +1434,7 @@ func (a *TransactionsApi) TransactionBatchUpdateCategoriesHandler(c *core.WebCon return nil, errs.ErrTransactionTypeInvalid } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) @@ -1548,9 +1570,19 @@ func (a *TransactionsApi) TransactionBatchUpdateAccountsHandler(c *core.WebConte return nil, errs.ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + newSourceAccount := accountMap[transaction.AccountId] + newDestinationAccount := accountMap[transaction.RelatedAccountId] - if !transactionEditable { + if !transactionBatchUpdateReq.IsDestinationAccount && transaction.AccountId != account.AccountId { + newSourceAccount = account + } else if transactionBatchUpdateReq.IsDestinationAccount && transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT && transaction.RelatedAccountId != account.AccountId { + newDestinationAccount = account + } + + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId]) + newTransactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, newSourceAccount, newDestinationAccount) + + if !transactionEditable || !newTransactionEditable { log.Warnf(c, "[transactions.TransactionBatchUpdateAccountsHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime } @@ -1651,6 +1683,13 @@ func (a *TransactionsApi) TransactionBatchAddTagsHandler(c *core.WebContext) (an return nil, errs.Or(err, errs.ErrOperationFailed) } + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, transactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionBatchAddTagsHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + transactionTagIndexes, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds) if err != nil { @@ -1668,7 +1707,7 @@ func (a *TransactionsApi) TransactionBatchAddTagsHandler(c *core.WebContext) (an return nil, errs.ErrTransactionTypeInvalid } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { log.Warnf(c, "[transactions.TransactionBatchAddTagsHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) @@ -1769,6 +1808,13 @@ func (a *TransactionsApi) TransactionBatchRemoveTagsHandler(c *core.WebContext) return nil, errs.Or(err, errs.ErrOperationFailed) } + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, transactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionBatchRemoveTagsHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + allTransactionIds := make([]int64, 0, len(transactions)) for i := 0; i < len(transactions); i++ { @@ -1779,7 +1825,7 @@ func (a *TransactionsApi) TransactionBatchRemoveTagsHandler(c *core.WebContext) return nil, errs.ErrTransactionTypeInvalid } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { log.Warnf(c, "[transactions.TransactionBatchRemoveTagsHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) @@ -1842,6 +1888,13 @@ func (a *TransactionsApi) TransactionBatchClearTagsHandler(c *core.WebContext) ( return nil, errs.Or(err, errs.ErrOperationFailed) } + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, transactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionBatchClearTagsHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + allTransactionIds := make([]int64, 0, len(transactions)) for i := 0; i < len(transactions); i++ { @@ -1852,7 +1905,7 @@ func (a *TransactionsApi) TransactionBatchClearTagsHandler(c *core.WebContext) ( return nil, errs.ErrTransactionTypeInvalid } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { log.Warnf(c, "[transactions.TransactionBatchClearTagsHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) @@ -1970,7 +2023,14 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er return nil, errs.ErrTransactionTypeInvalid } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, []*models.Transaction{transaction}) + + if err != nil { + log.Errorf(c, "[transactions.TransactionDeleteHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime @@ -2033,13 +2093,20 @@ func (a *TransactionsApi) TransactionBatchDeleteHandler(c *core.WebContext) (any return nil, errs.Or(err, errs.ErrOperationFailed) } + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, transactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionBatchDeleteHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + for i := 0; i < len(transactions); i++ { transaction := transactions[i] - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { - log.Warnf(c, "[transactions.TransactionBatchUpdateCategoriesHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) - return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime + log.Warnf(c, "[transactions.TransactionBatchDeleteHandler] transaction \"id:%d\" is not editable for user \"uid:%d\"", transaction.TransactionId, uid) + return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime } } @@ -2470,13 +2537,24 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er for i := 0; i < len(transactionImportReq.Transactions); i++ { transactionCreateReq := transactionImportReq.Transactions[i] transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP()) - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone) + newTransactions[i] = transaction + } + + allUsedAccounts, err := a.getTransactionUsedAccounts(c, uid, newTransactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionImportHandler] failed to get transaction used accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + for i := 0; i < len(newTransactions); i++ { + transaction := newTransactions[i] + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone, allUsedAccounts[transaction.AccountId], allUsedAccounts[transaction.RelatedAccountId]) if !transactionEditable { + log.Warnf(c, "[transactions.TransactionImportHandler] transaction \"index:%d\" is not editable for user \"uid:%d\"", i, uid) return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime } - - newTransactions[i] = transaction } err = a.transactions.BatchCreateTransactions(c, user.Uid, newTransactions, newTransactionTagIdsMap, func(currentProcess float64) { @@ -2708,6 +2786,41 @@ func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.We return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil } +func (a *TransactionsApi) getTransactionUsedAccounts(c *core.WebContext, uid int64, transactions []*models.Transaction) (map[int64]*models.Account, error) { + accountIds := make([]int64, 0, len(transactions)*2) + + for i := 0; i < len(transactions); i++ { + accountIds = append(accountIds, transactions[i].AccountId) + + if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { + accountIds = append(accountIds, transactions[i].RelatedAccountId) + } + } + + accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds)) + + if err != nil { + log.Errorf(c, "[transactions.getTransactionUsedAccounts] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + for i := 0; i < len(transactions); i++ { + if _, exists := accountMap[transactions[i].AccountId]; !exists { + log.Warnf(c, "[transactions.getTransactionUsedAccounts] account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transactions[i].TransactionId, uid) + return nil, errs.ErrSourceAccountNotFound + } + + if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { + if _, exists := accountMap[transactions[i].RelatedAccountId]; !exists { + log.Warnf(c, "[transactions.getTransactionUsedAccounts] related account of transaction \"id:%d\" does not exist for user \"uid:%d\"", transactions[i].TransactionId, uid) + return nil, errs.ErrDestinationAccountNotFound + } + } + } + + return accountMap, nil +} + func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, allAccounts map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) { result := make(models.TransactionInfoResponseSlice, len(transactions)) diff --git a/pkg/api/users.go b/pkg/api/users.go index 2e539403..c3c32949 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -256,6 +256,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname) modifyProfileBasicInfo := false + modifyUseLastReconciledTime := false anythingUpdate := false userNew := &models.User{ Uid: user.Uid, @@ -317,6 +318,16 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro anythingUpdate = true } + if userUpdateReq.UseLastReconciledTime != nil && *userUpdateReq.UseLastReconciledTime != user.UseLastReconciledTime { + user.UseLastReconciledTime = *userUpdateReq.UseLastReconciledTime + userNew.UseLastReconciledTime = *userUpdateReq.UseLastReconciledTime + modifyProfileBasicInfo = true + modifyUseLastReconciledTime = true + anythingUpdate = true + } else { + modifyUseLastReconciledTime = false + } + if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope { user.TransactionEditScope = *userUpdateReq.TransactionEditScope userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope @@ -531,7 +542,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro return nil, errs.ErrNothingWillBeUpdated } - keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage) + keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage, modifyUseLastReconciledTime) if err != nil { log.Errorf(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error()) diff --git a/pkg/mcp/add_transaction_tool_handler.go b/pkg/mcp/add_transaction_tool_handler.go index e747c6a4..41670280 100644 --- a/pkg/mcp/add_transaction_tool_handler.go +++ b/pkg/mcp/add_transaction_tool_handler.go @@ -178,7 +178,7 @@ func (h *mcpAddTransactionToolHandler) Handle(c *core.WebContext, callToolReq *M return nil, nil, err } - transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60), sourceAccount, destinationAccount) if !transactionEditable { return nil, nil, errs.ErrCannotCreateTransactionWithThisTransactionTime diff --git a/pkg/models/account.go b/pkg/models/account.go index dd8ec37a..61ecc52d 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -90,7 +90,8 @@ type Account struct { // AccountExtend represents account extend data stored in database type AccountExtend struct { - CreditCardStatementDate *int `json:"creditCardStatementDate"` + LastReconciledTime *int64 `json:"lastReconciledTime"` + CreditCardStatementDate *int `json:"creditCardStatementDate"` } // AccountCreateRequest represents all parameters of account creation request @@ -119,6 +120,7 @@ type AccountModifyRequest struct { Currency *string `json:"currency" binding:"omitempty,len=3,validCurrency"` Balance *int64 `json:"balance" binding:"omitempty"` BalanceTime *int64 `json:"balanceTime" binding:"omitempty"` + LastReconciledTime *int64 `json:"lastReconciledTime" binding:"omitempty"` Comment string `json:"comment" binding:"max=255"` CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"` Hidden bool `json:"hidden"` @@ -169,6 +171,7 @@ type AccountInfoResponse struct { Color string `json:"color"` Currency string `json:"currency"` Balance int64 `json:"balance"` + LastReconciledTime *int64 `json:"lastReconciledTime,omitempty"` Comment string `json:"comment"` CreditCardStatementDate *int `json:"creditCardStatementDate,omitempty"` DisplayOrder int32 `json:"displayOrder"` @@ -178,10 +181,24 @@ type AccountInfoResponse struct { SubAccounts AccountInfoResponseSlice `json:"subAccounts,omitempty"` } +// GetLastReconciledTime returns the last reconciled time of the account +func (a *Account) GetLastReconciledTime() int64 { + if a.Extend != nil && a.Extend.LastReconciledTime != nil { + return *a.Extend.LastReconciledTime + } + + return 0 +} + // ToAccountInfoResponse returns a view-object according to database model func (a *Account) ToAccountInfoResponse() *AccountInfoResponse { + var lastReconciledTime *int64 var creditCardStatementDate *int + if a.Extend != nil { + lastReconciledTime = a.Extend.LastReconciledTime + } + if a.ParentAccountId == LevelOneAccountParentId && a.Category == ACCOUNT_CATEGORY_CREDIT_CARD { if a.Extend != nil { creditCardStatementDate = a.Extend.CreditCardStatementDate @@ -201,6 +218,7 @@ func (a *Account) ToAccountInfoResponse() *AccountInfoResponse { Currency: a.Currency, Balance: a.Balance, Comment: a.Comment, + LastReconciledTime: lastReconciledTime, CreditCardStatementDate: creditCardStatementDate, DisplayOrder: a.DisplayOrder, IsAsset: assetAccountCategory[a.Category], diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index fd36d50c..fccfc826 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -549,10 +549,6 @@ func ParseTransactionTagFilter(tagFilterStr string) ([]*TransactionTagFilter, er // IsEditable returns whether this transaction can be edited func (t *Transaction) IsEditable(currentUser *User, clientTimezone *time.Location, account *Account, relatedAccount *Account) bool { - if currentUser == nil || !currentUser.CanEditTransactionByTransactionTime(t.TransactionTime, clientTimezone) { - return false - } - if account == nil || account.Hidden { return false } @@ -563,6 +559,10 @@ func (t *Transaction) IsEditable(currentUser *User, clientTimezone *time.Locatio } } + if currentUser == nil || !currentUser.CanEditTransactionByTransactionTime(t.TransactionTime, clientTimezone, account, relatedAccount) { + return false + } + return true } diff --git a/pkg/models/user.go b/pkg/models/user.go index da6f2ea2..73f4ab37 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -13,14 +13,15 @@ type TransactionEditScope byte // Editable Transaction Ranges const ( - TRANSACTION_EDIT_SCOPE_NONE TransactionEditScope = 0 - TRANSACTION_EDIT_SCOPE_ALL TransactionEditScope = 1 - TRANSACTION_EDIT_SCOPE_TODAY_OR_LATER TransactionEditScope = 2 - TRANSACTION_EDIT_SCOPE_LAST_24H_OR_LATER TransactionEditScope = 3 - TRANSACTION_EDIT_SCOPE_THIS_WEEK_OR_LATER TransactionEditScope = 4 - TRANSACTION_EDIT_SCOPE_THIS_MONTH_OR_LATER TransactionEditScope = 5 - TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER TransactionEditScope = 6 - TRANSACTION_EDIT_SCOPE_INVALID TransactionEditScope = 255 + TRANSACTION_EDIT_SCOPE_NONE TransactionEditScope = 0 + TRANSACTION_EDIT_SCOPE_ALL TransactionEditScope = 1 + TRANSACTION_EDIT_SCOPE_TODAY_OR_LATER TransactionEditScope = 2 + TRANSACTION_EDIT_SCOPE_LAST_24H_OR_LATER TransactionEditScope = 3 + TRANSACTION_EDIT_SCOPE_THIS_WEEK_OR_LATER TransactionEditScope = 4 + TRANSACTION_EDIT_SCOPE_THIS_MONTH_OR_LATER TransactionEditScope = 5 + TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER TransactionEditScope = 6 + TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER TransactionEditScope = 7 + TRANSACTION_EDIT_SCOPE_INVALID TransactionEditScope = 255 ) // String returns a textual representation of the editable transaction ranges enum @@ -40,6 +41,8 @@ func (s TransactionEditScope) String() string { return "ThisMonthOrLater" case TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER: return "ThisYearOrLater" + case TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER: + return "LastReconciledTimeOrLater" case TRANSACTION_EDIT_SCOPE_INVALID: return "Invalid" default: @@ -90,6 +93,7 @@ type User struct { Salt string `xorm:"VARCHAR(10) NOT NULL"` CustomAvatarType string `xorm:"VARCHAR(10)"` DefaultAccountId int64 + UseLastReconciledTime bool TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"` Language string `xorm:"VARCHAR(10)"` DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"` @@ -128,6 +132,7 @@ type UserBasicInfo struct { AvatarUrl string `json:"avatar"` AvatarProvider string `json:"avatarProvider,omitempty"` DefaultAccountId int64 `json:"defaultAccountId,string"` + UseLastReconciledTime bool `json:"useLastReconciledTime"` TransactionEditScope TransactionEditScope `json:"transactionEditScope"` Language string `json:"language"` DefaultCurrency string `json:"defaultCurrency"` @@ -194,7 +199,8 @@ type UserProfileUpdateRequest struct { Password string `json:"password" binding:"omitempty,min=6,max=128"` OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"` DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"` - TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=6"` + UseLastReconciledTime *bool `json:"useLastReconciledTime" binding:"omitempty"` + TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"` Language string `json:"language" binding:"omitempty,min=2,max=16"` DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"` FirstDayOfWeek *core.WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"` @@ -230,7 +236,7 @@ type UserProfileResponse struct { } // CanEditTransactionByTransactionTime returns whether this user can edit transaction with specified transaction time -func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, clientTimezone *time.Location) bool { +func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, clientTimezone *time.Location, account *Account, destinationAccount *Account) bool { if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_NONE { return false } else if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_ALL { @@ -242,14 +248,14 @@ func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, client transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transactionTime) if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_LAST_24H_OR_LATER { - return transactionUnixTime >= now.Add(-24*time.Hour).Unix() + return transactionUnixTime > now.Add(-24*time.Hour).Unix() } clientNow := now.In(clientTimezone) clientTodayStartTime := utils.GetStartOfDay(clientNow) if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_TODAY_OR_LATER { - return transactionUnixTime >= clientTodayStartTime.Unix() + return transactionUnixTime > clientTodayStartTime.Unix() } else if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_THIS_WEEK_OR_LATER { dayOfWeek := int(now.Weekday()) - int(u.FirstDayOfWeek) @@ -258,13 +264,29 @@ func (u *User) CanEditTransactionByTransactionTime(transactionTime int64, client } clientWeekStartTime := clientTodayStartTime.AddDate(0, 0, -dayOfWeek) - return transactionUnixTime >= clientWeekStartTime.Unix() + return transactionUnixTime > clientWeekStartTime.Unix() } else if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_THIS_MONTH_OR_LATER { clientMonthStartTime := clientTodayStartTime.AddDate(0, 0, -(now.Day() - 1)) - return transactionUnixTime >= clientMonthStartTime.Unix() + return transactionUnixTime > clientMonthStartTime.Unix() } else if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER { clientYearStartTime := clientTodayStartTime.AddDate(0, 0, -(now.YearDay() - 1)) - return transactionUnixTime >= clientYearStartTime.Unix() + return transactionUnixTime > clientYearStartTime.Unix() + } else if u.TransactionEditScope == TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER && u.UseLastReconciledTime { + minAccountLastReconciledTime := int64(0) + + if account != nil { + minAccountLastReconciledTime = account.GetLastReconciledTime() + } + + if destinationAccount != nil { + destinationAccountLastReconciledTime := destinationAccount.GetLastReconciledTime() + + if destinationAccountLastReconciledTime > minAccountLastReconciledTime { + minAccountLastReconciledTime = destinationAccountLastReconciledTime + } + } + + return transactionUnixTime > minAccountLastReconciledTime } return false @@ -285,6 +307,7 @@ func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, avata AvatarUrl: avatarUrl, AvatarProvider: string(avatarProvider), DefaultAccountId: u.DefaultAccountId, + UseLastReconciledTime: u.UseLastReconciledTime, TransactionEditScope: u.TransactionEditScope, Language: u.Language, DefaultCurrency: u.DefaultCurrency, diff --git a/pkg/models/user_test.go b/pkg/models/user_test.go index e47fcd22..01e7759a 100644 --- a/pkg/models/user_test.go +++ b/pkg/models/user_test.go @@ -16,7 +16,7 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsNone(t *testing.T) { } timezone := time.FixedZone("Timezone", int(utils.GetServerTimezoneOffsetMinutes())*60) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(time.Now().Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(time.Now().Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsAll(t *testing.T) { @@ -25,7 +25,7 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsAll(t *testing.T) { } timezone := time.FixedZone("Timezone", int(utils.GetServerTimezoneOffsetMinutes())*60) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(time.Now().Unix()), timezone)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(time.Now().Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsTodayOrLater(t *testing.T) { @@ -39,9 +39,10 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsTodayOrLater(t *testing. yesterdayLastDatetime := todayFirstDatetime.Add(-1 * time.Second) todayLastDatetime := yesterdayLastDatetime.Add(24 * time.Hour) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(todayFirstDatetime.Unix()), timezone)) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(todayLastDatetime.Unix()), timezone)) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(yesterdayLastDatetime.Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(todayFirstDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(todayFirstDatetime.Add(1*time.Second).Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(todayLastDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(yesterdayLastDatetime.Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsLast24HourOrLater(t *testing.T) { @@ -53,8 +54,9 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsLast24HourOrLater(t *tes timezone := time.FixedZone("Timezone", int(utils.GetServerTimezoneOffsetMinutes())*60) twentyfourHourBeforeDatetime := now.Add(-24 * time.Hour).Add(-1 * time.Second) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(twentyfourHourBeforeDatetime.Unix()), timezone)) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(twentyfourHourBeforeDatetime.Add(1*time.Second).Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(twentyfourHourBeforeDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(twentyfourHourBeforeDatetime.Add(1*time.Second).Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(twentyfourHourBeforeDatetime.Add(2*time.Second).Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsThisWeekOrLater(t *testing.T) { @@ -76,9 +78,10 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsThisWeekOrLater(t *testi lastWeekLastDatetime := thisWeekFirstDatetime.Add(-1 * time.Second) thisWeekLastDatetime := lastWeekLastDatetime.Add(24 * time.Hour) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisWeekFirstDatetime.Unix()), timezone)) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisWeekLastDatetime.Unix()), timezone)) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastWeekLastDatetime.Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisWeekFirstDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisWeekFirstDatetime.Add(1*time.Second).Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisWeekLastDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastWeekLastDatetime.Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsThisMonthOrLater(t *testing.T) { @@ -92,9 +95,10 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsThisMonthOrLater(t *test lastMonthLastDatetime := thisMonthFirstDatetime.Add(-1 * time.Second) thisMonthLastDatetime := lastMonthLastDatetime.Add(24 * time.Hour) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisMonthFirstDatetime.Unix()), timezone)) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisMonthLastDatetime.Unix()), timezone)) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastMonthLastDatetime.Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisMonthFirstDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisMonthFirstDatetime.Add(1*time.Second).Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisMonthLastDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastMonthLastDatetime.Unix()), timezone, nil, nil)) } func TestUserCanEditTransactionByTransactionTime_ScopeIsThisYearOrLater(t *testing.T) { @@ -108,7 +112,72 @@ func TestUserCanEditTransactionByTransactionTime_ScopeIsThisYearOrLater(t *testi lastYearLastDatetime := thisYearFirstDatetime.Add(-1 * time.Second) thisYearLastDatetime := lastYearLastDatetime.Add(24 * time.Hour) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisYearFirstDatetime.Unix()), timezone)) - assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisYearLastDatetime.Unix()), timezone)) - assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastYearLastDatetime.Unix()), timezone)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisYearFirstDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisYearFirstDatetime.Add(1*time.Second).Unix()), timezone, nil, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(thisYearLastDatetime.Unix()), timezone, nil, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(lastYearLastDatetime.Unix()), timezone, nil, nil)) +} + +func TestUserCanEditTransactionByTransactionTime_ScopeIsLastReconciledTimeOrLater(t *testing.T) { + user := &User{ + TransactionEditScope: TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER, + UseLastReconciledTime: true, + } + + now := time.Now() + timezone := time.FixedZone("Timezone", int(utils.GetServerTimezoneOffsetMinutes())*60) + sourceAccountLastReconciledTime := now.Add(-24 * time.Hour) + sourceAccountLastRecondiledUnixTime := sourceAccountLastReconciledTime.Unix() + sourceAccount := &Account{ + Extend: &AccountExtend{ + LastReconciledTime: &sourceAccountLastRecondiledUnixTime, + }, + } + destinationAccountLastReconciledTime := now.Add(-20 * time.Hour) + destinationAccountLastReconciledUnixTime := destinationAccountLastReconciledTime.Unix() + destinationAccount := &Account{ + Extend: &AccountExtend{ + LastReconciledTime: &destinationAccountLastReconciledUnixTime, + }, + } + + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Add(-1*time.Second).Unix()), timezone, sourceAccount, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Unix()), timezone, sourceAccount, nil)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Add(1*time.Second).Unix()), timezone, sourceAccount, nil)) + + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Add(-1*time.Second).Unix()), timezone, sourceAccount, destinationAccount)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Unix()), timezone, sourceAccount, destinationAccount)) + assert.Equal(t, true, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Add(1*time.Second).Unix()), timezone, sourceAccount, destinationAccount)) +} + +func TestUserCanEditTransactionByTransactionTime_ScopeIsLastReconciledTimeOrLaterButUserDoesNotUseLastReconciledTime(t *testing.T) { + user := &User{ + TransactionEditScope: TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER, + UseLastReconciledTime: false, + } + + now := time.Now() + timezone := time.FixedZone("Timezone", int(utils.GetServerTimezoneOffsetMinutes())*60) + sourceAccountLastReconciledTime := now.Add(-24 * time.Hour) + sourceAccountLastRecondiledUnixTime := sourceAccountLastReconciledTime.Unix() + sourceAccount := &Account{ + Extend: &AccountExtend{ + LastReconciledTime: &sourceAccountLastRecondiledUnixTime, + }, + } + destinationAccountLastReconciledTime := now.Add(-20 * time.Hour) + destinationAccountLastReconciledUnixTime := destinationAccountLastReconciledTime.Unix() + destinationAccount := &Account{ + Extend: &AccountExtend{ + LastReconciledTime: &destinationAccountLastReconciledUnixTime, + }, + } + + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Add(-1*time.Second).Unix()), timezone, sourceAccount, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Unix()), timezone, sourceAccount, nil)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(sourceAccountLastReconciledTime.Add(1*time.Second).Unix()), timezone, sourceAccount, nil)) + + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Add(-1*time.Second).Unix()), timezone, sourceAccount, destinationAccount)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Unix()), timezone, sourceAccount, destinationAccount)) + assert.Equal(t, false, user.CanEditTransactionByTransactionTime(utils.GetMinTransactionTimeFromUnixTime(destinationAccountLastReconciledTime.Add(1*time.Second).Unix()), timezone, sourceAccount, destinationAccount)) } diff --git a/pkg/services/users.go b/pkg/services/users.go index 1a455a18..374e8a99 100644 --- a/pkg/services/users.go +++ b/pkg/services/users.go @@ -234,7 +234,7 @@ func (s *UserService) CreateUser(c core.Context, user *models.User, noPassword b } // UpdateUser saves an existed user model to database -func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLanguage bool) (keyProfileUpdated bool, emailSetToUnverified bool, err error) { +func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLanguage bool, modifyUseLastReconciledTime bool) (keyProfileUpdated bool, emailSetToUnverified bool, err error) { if user.Uid <= 0 { return false, false, errs.ErrUserIdInvalid } @@ -277,7 +277,11 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa updateCols = append(updateCols, "default_account_id") } - if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_THIS_YEAR_OR_LATER { + if modifyUseLastReconciledTime { + updateCols = append(updateCols, "use_last_reconciled_time") + } + + if models.TRANSACTION_EDIT_SCOPE_NONE <= user.TransactionEditScope && user.TransactionEditScope <= models.TRANSACTION_EDIT_SCOPE_LAST_RECONCILED_TIME_OR_LATER { updateCols = append(updateCols, "transaction_edit_scope") } diff --git a/src/components/desktop/DateTimeSelect.vue b/src/components/desktop/DateTimeSelect.vue index cca06d1b..c5983c89 100644 --- a/src/components/desktop/DateTimeSelect.vue +++ b/src/components/desktop/DateTimeSelect.vue @@ -3,6 +3,7 @@ persistent-placeholder :readonly="readonly" :disabled="disabled" + :clearable="!emptyValue ? clearable : false" :label="label" :menu-props="{ contentClass: 'date-time-select-menu' }" v-model="dateTime" @@ -107,13 +108,16 @@ import { setChildInputFocus } from '@/lib/ui/desktop.ts'; const props = defineProps<{ modelValue: number; timezoneUtcOffset: number; + emptyValue?: boolean; disabled?: boolean; readonly?: boolean; + clearable?: boolean; label?: string; }>(); const emit = defineEmits<{ (e: 'update:modelValue', value: number): void; + (e: 'clear:modelValue'): void; (e: 'error', message: string): void; }>(); @@ -154,7 +158,12 @@ const dateTime = computed({ get: () => { return getLocalDatetimeFromSameDateTimeOfUnixTime(props.modelValue, props.timezoneUtcOffset); }, - set: (value: Date) => { + set: (value: Date | null) => { + if (!value) { + emit('clear:modelValue'); + return; + } + const unixTime = getUnixTimeFromSameDateTimeOfLocalDatetime(value, props.timezoneUtcOffset); if (unixTime < 0) { @@ -166,7 +175,7 @@ const dateTime = computed({ } }); -const displayTime = computed(() => formatDateTimeToLongDateTime(parseDateTimeFromUnixTimeWithTimezoneOffset(props.modelValue, props.timezoneUtcOffset))); +const displayTime = computed(() => props.emptyValue ? tt('None') : formatDateTimeToLongDateTime(parseDateTimeFromUnixTimeWithTimezoneOffset(props.modelValue, props.timezoneUtcOffset))); const hourItems = computed(() => generateAllHours(1, isHourTwoDigits.value)); const minuteItems = computed(() => generateAllMinutesOrSeconds(1, isMinuteTwoDigits.value)); diff --git a/src/components/mobile/DateTimeSelectionSheet.vue b/src/components/mobile/DateTimeSelectionSheet.vue index 82ca3748..8175a7bf 100644 --- a/src/components/mobile/DateTimeSelectionSheet.vue +++ b/src/components/mobile/DateTimeSelectionSheet.vue @@ -4,7 +4,8 @@
- + +
@@ -122,11 +123,13 @@ const props = defineProps<{ modelValue: number; timezoneUtcOffset: number; initMode?: string; + clearable?: boolean; show: boolean; }>(); const emit = defineEmits<{ (e: 'update:modelValue', value: number): void; + (e: 'clear:modelValue'): void; (e: 'update:show', value: boolean): void; }>(); @@ -221,6 +224,11 @@ function setCurrentTime(): void { } } +function clear(): void { + emit('clear:modelValue'); + emit('update:show', false); +} + function confirm(): void { if (!dateTime.value) { return; diff --git a/src/core/transaction.ts b/src/core/transaction.ts index ed821a3a..ef7b7eab 100644 --- a/src/core/transaction.ts +++ b/src/core/transaction.ts @@ -14,27 +14,46 @@ export enum TransactionRelatedAccountType { export class TransactionEditScopeType implements TypeAndName { private static readonly allInstances: TransactionEditScopeType[] = []; + private static readonly allInstancesWithoutReconciledTime: TransactionEditScopeType[] = []; + private static readonly allInstancesByType: Record = {}; - public static readonly None = new TransactionEditScopeType(0, 'None'); - public static readonly All = new TransactionEditScopeType(1, 'All'); - public static readonly TodayOrLater = new TransactionEditScopeType(2, 'Today or later'); - public static readonly Recent24HoursOrLater = new TransactionEditScopeType(3, 'Recent 24 hours or later'); - public static readonly ThisWeekOrLater = new TransactionEditScopeType(4, 'This week or later'); - public static readonly ThisMonthOrLater = new TransactionEditScopeType(5, 'This month or later'); - public static readonly ThisYearOrLater = new TransactionEditScopeType(6, 'This year or later'); + public static readonly None = new TransactionEditScopeType(0, 'None', false); + public static readonly All = new TransactionEditScopeType(1, 'All', false); + public static readonly TodayOrLater = new TransactionEditScopeType(2, 'Today or later', false); + public static readonly Recent24HoursOrLater = new TransactionEditScopeType(3, 'Recent 24 hours or later', false); + public static readonly ThisWeekOrLater = new TransactionEditScopeType(4, 'This week or later', false); + public static readonly ThisMonthOrLater = new TransactionEditScopeType(5, 'This month or later', false); + public static readonly ThisYearOrLater = new TransactionEditScopeType(6, 'This year or later', false); + public static readonly LastReconciledTimeOrlater = new TransactionEditScopeType(7, 'Last reconciled time or later', true); public readonly type: number; public readonly name: string; + public readonly needLastReconciledTime: boolean; - private constructor(type: number, name: string) { + private constructor(type: number, name: string, needLastReconciledTime: boolean) { this.type = type; this.name = name; + this.needLastReconciledTime = needLastReconciledTime; TransactionEditScopeType.allInstances.push(this); + + if (!needLastReconciledTime) { + TransactionEditScopeType.allInstancesWithoutReconciledTime.push(this); + } + + TransactionEditScopeType.allInstancesByType[type] = this; } - public static values(): TransactionEditScopeType[] { - return TransactionEditScopeType.allInstances; + public static values(useLastReconciledTime: boolean): TransactionEditScopeType[] { + if (useLastReconciledTime) { + return TransactionEditScopeType.allInstances; + } else { + return TransactionEditScopeType.allInstancesWithoutReconciledTime; + } + } + + public static valueOf(type: number): TransactionEditScopeType | undefined { + return TransactionEditScopeType.allInstancesByType[type]; } } diff --git a/src/locales/de.json b/src/locales/de.json index b41bf570..fd86ba7a 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Dezimaltrennzeichen", "Digit Grouping Symbol": "Zifferngruppierungssymbol", "Digit Grouping": "Zifferngruppierung", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Bearbeitbarer Transaktionsbereich", "Today or later": "Heute oder später", "Recent 24 hours or later": "Letzte 24 Stunden oder später", "This week or later": "Diese Woche oder später", "This month or later": "Dieser Monat oder später", "This year or later": "Dieses Jahr oder später", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Anmelden", "Log in with OAuth 2.0": "Mit OAuth 2.0 anmelden", "Log in with Connect ID": "Mit Connect ID anmelden", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Ausstehender Teilkontostand", "Balance Time": "Saldozeit", "Sub-account Balance Time": "Teilkontosaldozeit", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Abrechnungsdatum", "Description": "Beschreibung", "Your account description (optional)": "Ihre Kontobeschreibung (optional)", diff --git a/src/locales/en.json b/src/locales/en.json index b2c3d5f4..fd19c4dd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Decimal Separator", "Digit Grouping Symbol": "Digit Grouping Symbol", "Digit Grouping": "Digit Grouping", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Editable Transaction Range", "Today or later": "Today or later", "Recent 24 hours or later": "Recent 24 hours or later", "This week or later": "This week or later", "This month or later": "This month or later", "This year or later": "This year or later", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Log In", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Sub-account Outstanding Balance", "Balance Time": "Balance Time", "Sub-account Balance Time": "Sub-account Balance Time", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Statement Date", "Description": "Description", "Your account description (optional)": "Your account description (optional)", diff --git a/src/locales/es.json b/src/locales/es.json index 63b749a6..e1235ac1 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Separador Decimal", "Digit Grouping Symbol": "Símbolo de Agrupación de Dígitos", "Digit Grouping": "Agrupación de Dígitos", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Rango de Transacciones Editables", "Today or later": "Hoy o más tarde", "Recent 24 hours or later": "Últimas 24 horas o más", "This week or later": "Esta semana o más tarde", "This month or later": "Este mes o más tarde", "This year or later": "Este año o más tarde", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Acceso", "Log in with OAuth 2.0": "Iniciar sesión con OAuth 2.0", "Log in with Connect ID": "Inicie sesión con Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Saldo pendiente de la Subcuenta", "Balance Time": "Fecha del Saldo", "Sub-account Balance Time": "Fecha del Saldo de la Subcuenta", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Fecha del Extracto", "Description": "Descripción", "Your account description (optional)": "Descripción de tu cuenta (opcional)", diff --git a/src/locales/fr.json b/src/locales/fr.json index 7d4d85a5..d6b1acff 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Séparateur décimal", "Digit Grouping Symbol": "Symbole de regroupement des chiffres", "Digit Grouping": "Regroupement des chiffres", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Plage de transactions modifiables", "Today or later": "Aujourd'hui ou plus tard", "Recent 24 hours or later": "24 dernières heures ou plus tard", "This week or later": "Cette semaine ou plus tard", "This month or later": "Ce mois ou plus tard", "This year or later": "Cette année ou plus tard", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Se connecter", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Solde impayé du sous-compte", "Balance Time": "Heure du solde", "Sub-account Balance Time": "Heure du solde du sous-compte", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Date de relevé", "Description": "Description", "Your account description (optional)": "Description de votre compte (optionnel)", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index a44ce9bf..9256c670 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -2564,7 +2564,7 @@ export function useI18n() { getAllStatisticsSortingTypes: (useAlternativeName?: boolean) => getLocalizedDisplayNameAndType(ChartSortingType.values(), useAlternativeName), getAllStatisticsDateAggregationTypes: (analysisType: StatisticsAnalysisType, includeBillingCycle: boolean) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, true, includeBillingCycle), getAllStatisticsDateAggregationTypesWithShortName: (analysisType: StatisticsAnalysisType, includeBillingCycle: boolean) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, false, includeBillingCycle), - getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()), + getAllTransactionEditScopeTypes: (useLastReconciledTime: boolean) => getLocalizedDisplayNameAndType(TransactionEditScopeType.values(useLastReconciledTime)), getAllTransactionQuickSaveButtonStyles: () => getLocalizedDisplayNameAndType(TransactionQuickSaveButtonStyle.values()), getAllTransactionQuickAddButtonActionTypes: () => getLocalizedDisplayNameAndType(TransactionQuickAddButtonActionType.values()), getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()), @@ -2579,6 +2579,7 @@ export function useI18n() { getAllTransactionExplorerChartTypes: (operators?: TransactionExplorerChartType[]) => getLocalizedNameValue(operators ?? TransactionExplorerChartType.values()), // get localized info getLanguageInfo, + getEnableDisableOption: (value: boolean) => t(value ? 'Enabled' : 'Disabled'), getMonthShortName, getMonthLongName, getMonthdayOrdinal, diff --git a/src/locales/it.json b/src/locales/it.json index aef4e117..893d770b 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Separatore decimale", "Digit Grouping Symbol": "Simbolo di raggruppamento cifre", "Digit Grouping": "Raggruppamento cifre", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Intervallo di modifica transazioni", "Today or later": "Oggi o successivo", "Recent 24 hours or later": "Ultime 24 ore o successivo", "This week or later": "Questa settimana o successiva", "This month or later": "Questo mese o successivo", "This year or later": "Quest'anno o successivo", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Accedi", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Saldo residuo sotto-account", "Balance Time": "Ora saldo", "Sub-account Balance Time": "Ora saldo sotto-account", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Data estratto conto", "Description": "Descrizione", "Your account description (optional)": "La descrizione del tuo account (opzionale)", diff --git a/src/locales/ja.json b/src/locales/ja.json index 123e914f..e80af7b0 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "小数点", "Digit Grouping Symbol": "桁区切り記号", "Digit Grouping": "桁区切り位置", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "編集可能な取引範囲", "Today or later": "今日以降", "Recent 24 hours or later": "直近24時間以降", "This week or later": "今週以降", "This month or later": "今月以降", "This year or later": "今年以降", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "ログイン", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "サブ口座の未払い残高", "Balance Time": "残高時間", "Sub-account Balance Time": "サブ口座残高時間", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "明細日", "Description": "説明", "Your account description (optional)": "口座の説明(オプション)", diff --git a/src/locales/kn.json b/src/locales/kn.json index fbcb4db5..8f47cc94 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "ದಶಾಂಶ ವಿಭಜಕ", "Digit Grouping Symbol": "ಅಂಕ ವಿಂಗಡಣ ಚಿಹ್ನೆ", "Digit Grouping": "ಅಂಕ ವಿಂಗಡಣೆ", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "ತಿದ್ದುಪಡಿ ಮಾಡಬಹುದಾದ ವಹಿವಾಟಿನ ವ್ಯಾಪ್ತಿ", "Today or later": "ಇಂದು ಅಥವಾ ನಂತರ", "Recent 24 hours or later": "ಕಳೆದ 24 ಗಂಟೆಗಳು ಅಥವಾ ನಂತರ", "This week or later": "ಈ ವಾರ ಅಥವಾ ನಂತರ", "This month or later": "ಈ ತಿಂಗಳು ಅಥವಾ ನಂತರ", "This year or later": "ಈ ವರ್ಷ ಅಥವಾ ನಂತರ", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "ಲಾಗ್ ಇನ್", "Log in with OAuth 2.0": "OAuth 2.0 ಮೂಲಕ ಲಾಗ್ ಇನ್ ಆಗಿ", "Log in with Connect ID": "Connect ID ಮೂಲಕ ಲಾಗ್ ಇನ್ ಆಗಿ", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "ಉಪ-ಖಾತೆ ಬಾಕಿ ಶೇಷ", "Balance Time": "ಶೇಷ ಸಮಯ", "Sub-account Balance Time": "ಉಪ-ಖಾತೆ ಶೇಷ ಸಮಯ", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "ಸ್ಟೇಟ್ಮೆಂಟ್ ದಿನಾಂಕ", "Description": "ವಿವರಣೆ", "Your account description (optional)": "ನಿಮ್ಮ ಖಾತೆ ವಿವರಣೆ (ಐಚ್ಛಿಕ)", diff --git a/src/locales/ko.json b/src/locales/ko.json index cb7c0d61..d4780ede 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "소수점 구분자", "Digit Grouping Symbol": "자리 구분 기호", "Digit Grouping": "자리 구분", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "편집 가능한 거래 범위", "Today or later": "오늘 이후", "Recent 24 hours or later": "최근 24시간 이후", "This week or later": "이번 주 이후", "This month or later": "이번 달 이후", "This year or later": "올해 이후", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "로그인", "Log in with OAuth 2.0": "OAuth 2.0으로 로그인", "Log in with Connect ID": "Connect ID로 로그인", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "하위계좌 미결제 잔액", "Balance Time": "잔액 시간", "Sub-account Balance Time": "하위계좌 잔액 시간", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "명세서 날짜", "Description": "설명", "Your account description (optional)": "계좌 설명 (선택 사항)", diff --git a/src/locales/nl.json b/src/locales/nl.json index 15b2aa22..a6603a6d 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Decimaalteken", "Digit Grouping Symbol": "Scheidingsteken voor duizendtallen", "Digit Grouping": "Groeperen van cijfers", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Bewerkbare transactiebereik", "Today or later": "Vandaag of later", "Recent 24 hours or later": "Afgelopen 24 uur of later", "This week or later": "Deze week of later", "This month or later": "Deze maand of later", "This year or later": "Dit jaar of later", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Inloggen", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Openstaand saldo subrekening", "Balance Time": "Saldo-tijdstip", "Sub-account Balance Time": "Saldo-tijdstip subrekening", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Afsluitdatum", "Description": "Beschrijving", "Your account description (optional)": "Je rekeningbeschrijving (optioneel)", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 5aea997f..4fe370e6 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Separador Decimal", "Digit Grouping Symbol": "Símbolo de Agrupamento de Dígitos", "Digit Grouping": "Agrupamento de Dígitos", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Período Editável de Transações", "Today or later": "A partir de hoje", "Recent 24 hours or later": "A partir das últimas 24 horas", "This week or later": "A partir desta semana", "This month or later": "A partir deste mês", "This year or later": "A partir deste ano", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Fazer Login", "Log in with OAuth 2.0": "Entrar com OAuth 2.0", "Log in with Connect ID": "Entrar com Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Saldo em Aberto da Subconta", "Balance Time": "Hora do Saldo", "Sub-account Balance Time": "Hora do Saldo da Subconta", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Data do Extrato", "Description": "Descrição", "Your account description (optional)": "Descrição da sua conta (opcional)", diff --git a/src/locales/ru.json b/src/locales/ru.json index b6ce8402..651e7ce6 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Разделитель десятичных", "Digit Grouping Symbol": "Символ группировки цифр", "Digit Grouping": "Группировка цифр", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Диапазон редактируемых транзакций", "Today or later": "Сегодня или позже", "Recent 24 hours or later": "Последние 24 часа или позже", "This week or later": "На этой неделе или позже", "This month or later": "В этом месяце или позже", "This year or later": "В этом году или позже", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Войти", "Log in with OAuth 2.0": "Войти с OAuth 2.0", "Log in with Connect ID": "Войти с Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Невыплаченные остаток субсчета", "Balance Time": "Время баланса", "Sub-account Balance Time": "Время баланса субсчета", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Дата выписки", "Description": "Описание", "Your account description (optional)": "Описание вашего счета (необязательно)", diff --git a/src/locales/sl.json b/src/locales/sl.json index e2f62ccc..47fdb040 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Decimalno ločilo", "Digit Grouping Symbol": "Ločilo tisočic", "Digit Grouping": "Združevanje števil", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Obseg urejanja transakcij", "Today or later": "Danes ali pozneje", "Recent 24 hours or later": "Zadnjih 24 ur ali pozneje", "This week or later": "Ta teden ali pozneje", "This month or later": "Ta mesec ali pozneje", "This year or later": "Letos ali pozneje", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Prijava", "Log in with OAuth 2.0": "Prijava z OAuth 2.0", "Log in with Connect ID": "Prijava s Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Neporavnano stanje na podračunu", "Balance Time": "Čas stanja", "Sub-account Balance Time": "Čas stanja na podračunu", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Datum izpiska", "Description": "Opis", "Your account description (optional)": "Opis vašega računa (neobvezno)", diff --git a/src/locales/ta.json b/src/locales/ta.json index 05268230..a7ce730f 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "தசம பிரிப்பான்", "Digit Grouping Symbol": "இலக்க குழு குறியீடு", "Digit Grouping": "இலக்க குழுவாக்கம்", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "திருத்துபடி செய்யக்கூடிய பரிவர்த்தனையின் வரம்பு", "Today or later": "இன்று அல்லது பின்பு", "Recent 24 hours or later": "கடந்த 24 மணி நேரம் அல்லது பின்பு", "This week or later": "இந்த வாரம் அல்லது பின்பு", "This month or later": "இந்த மாதம் அல்லது பின்பு", "This year or later": "இந்த வருடம் அல்லது பின்பு", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "உள்நுழை", "Log in with OAuth 2.0": "OAuth 2.0 மூலக உள்நுழை ஆக", "Log in with Connect ID": "Connect ID மூலக உள்நுழை ஆக", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "துணை-கணக்கு நிலுவை இருப்பு", "Balance Time": "இருப்பு நேரம்", "Sub-account Balance Time": "துணை-கணக்கு இருப்பு நேரம்", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "அறிக்கை தேதி", "Description": "விளக்கம்", "Your account description (optional)": "உங்கள் கணக்கு விளக்கம் (விருப்பமான)", diff --git a/src/locales/th.json b/src/locales/th.json index 48ba8a44..9b589cf3 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "ตัวคั่นทศนิยม", "Digit Grouping Symbol": "สัญลักษณ์จัดกลุ่มตัวเลข", "Digit Grouping": "จัดกลุ่มตัวเลข", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "ช่วงรายการที่แก้ไขได้", "Today or later": "วันนี้หรือหลังจากนี้", "Recent 24 hours or later": "24 ชั่วโมงที่ผ่านมา หรือหลังจากนี้", "This week or later": "สัปดาห์นี้หรือหลังจากนี้", "This month or later": "เดือนนี้หรือหลังจากนี้", "This year or later": "ปีนี้หรือหลังจากนี้", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "เข้าสู่ระบบ", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "ยอดค้างชำระบัญชีย่อย", "Balance Time": "เวลายอดคงเหลือ", "Sub-account Balance Time": "เวลายอดคงเหลือบัญชีย่อย", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "วันรายการ", "Description": "รายละเอียด", "Your account description (optional)": "รายละเอียดบัญชีของคุณ (ไม่บังคับ)", diff --git a/src/locales/tr.json b/src/locales/tr.json index 4711a923..ebff9fc0 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Ondalık Ayırıcı", "Digit Grouping Symbol": "Basamak Gruplama Sembolü", "Digit Grouping": "Basamak Gruplama", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Düzenlenebilir İşlem Aralığı", "Today or later": "Bugün veya sonrası", "Recent 24 hours or later": "Son 24 saat veya sonrası", "This week or later": "Bu hafta veya sonrası", "This month or later": "Bu ay veya sonrası", "This year or later": "Bu yıl veya sonrası", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Giriş Yap", "Log in with OAuth 2.0": "OAuth 2.0 ile giriş yap", "Log in with Connect ID": "Connect ID ile giriş yap", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Alt Hesap Kalan Borç Bakiyesi", "Balance Time": "Bakiye Zamanı", "Sub-account Balance Time": "Alt Hesap Bakiye Zamanı", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Ekstre Tarihi", "Description": "Açıklama", "Your account description (optional)": "Hesap açıklamanız (isteğe bağlı)", diff --git a/src/locales/uk.json b/src/locales/uk.json index d3738126..669ef29f 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Десятковий роздільник", "Digit Grouping Symbol": "Символ групування цифр", "Digit Grouping": "Групування цифр", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Діапазон редагованих транзакцій", "Today or later": "Сьогодні або пізніше", "Recent 24 hours or later": "Останні 24 години або пізніше", "This week or later": "Цього тижня або пізніше", "This month or later": "Цього місяця або пізніше", "This year or later": "Цього року або пізніше", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Увійти", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Несплачений баланс субрахунку", "Balance Time": "Час балансу", "Sub-account Balance Time": "Час балансу субрахунку", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Дата виписки", "Description": "Опис", "Your account description (optional)": "Опис вашого рахунку (необов'язково)", diff --git a/src/locales/vi.json b/src/locales/vi.json index 2791d991..fb254561 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "Dấu phân cách thập phân", "Digit Grouping Symbol": "Ký hiệu nhóm chữ số", "Digit Grouping": "Nhóm chữ số", + "Use Last Reconciled Time": "Use Last Reconciled Time", "Editable Transaction Range": "Phạm vi giao dịch có thể chỉnh sửa", "Today or later": "Hôm nay trở đi", "Recent 24 hours or later": "24 giờ gần đây trở đi", "This week or later": "Tuần này trở đi", "This month or later": "Tháng này trở đi", "This year or later": "Năm nay trở đi", + "Last reconciled time or later": "Last reconciled time or later", "Log In": "Đăng nhập", "Log in with OAuth 2.0": "Log in with OAuth 2.0", "Log in with Connect ID": "Log in with Connect ID", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "Sub-account Outstanding Balance", "Balance Time": "Thời gian số dư", "Sub-account Balance Time": "Thời gian số dư tài khoản phụ", + "Last Reconciled Time": "Last Reconciled Time", + "Sub-account Last Reconciled Time": "Sub-account Last Reconciled Time", "Statement Date": "Statement Date", "Description": "Mô tả", "Your account description (optional)": "Mô tả tài khoản của bạn (tùy chọn)", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 339a7861..1384776f 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "小数点", "Digit Grouping Symbol": "数字分组符号", "Digit Grouping": "数字分组", + "Use Last Reconciled Time": "使用最后对账时间", "Editable Transaction Range": "可编辑交易范围", "Today or later": "今天或更晚", "Recent 24 hours or later": "最近24小时或更晚", "This week or later": "本周或更晚", "This month or later": "本月或更晚", "This year or later": "今年或更晚", + "Last reconciled time or later": "最后对账时间或更晚", "Log In": "登录", "Log in with OAuth 2.0": "使用 OAuth 2.0 登录", "Log in with Connect ID": "使用 Connect ID 登录", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "子账户未清余额", "Balance Time": "余额时间", "Sub-account Balance Time": "子账户余额时间", + "Last Reconciled Time": "最后对账时间", + "Sub-account Last Reconciled Time": "子账户最后对账时间", "Statement Date": "账单日", "Description": "描述", "Your account description (optional)": "你的账户描述 (可选)", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 6188c713..1089eb11 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1683,12 +1683,14 @@ "Decimal Separator": "小數點", "Digit Grouping Symbol": "數字分組符號", "Digit Grouping": "數字分組", + "Use Last Reconciled Time": "使用最後對帳時間", "Editable Transaction Range": "可編輯交易範圍", "Today or later": "今天或更晚", "Recent 24 hours or later": "最近24小時或更晚", "This week or later": "本週或更晚", "This month or later": "本月或更晚", "This year or later": "今年或更晚", + "Last reconciled time or later": "最後對帳時間或更晚", "Log In": "登入", "Log in with OAuth 2.0": "使用 OAuth 2.0 登入", "Log in with Connect ID": "使用 Connect ID 登入", @@ -1922,6 +1924,8 @@ "Sub-account Outstanding Balance": "子帳戶未清餘額", "Balance Time": "餘額時間", "Sub-account Balance Time": "子帳戶餘額時間", + "Last Reconciled Time": "最後對帳時間", + "Sub-account Last Reconciled Time": "子帳戶最後對帳時間", "Statement Date": "帳單日", "Description": "描述", "Your account description (optional)": "您的帳戶描述 (選填)", diff --git a/src/models/account.ts b/src/models/account.ts index 830ff2c0..207664af 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -15,6 +15,7 @@ export class Account implements AccountInfoResponse { public currency: string; public balance: number; public balanceTime?: number; + public lastReconciledTime?: number; public comment: string; public creditCardStatementDate?: number; public displayOrder: number; @@ -24,7 +25,7 @@ export class Account implements AccountInfoResponse { private readonly _isAsset?: boolean; private readonly _isLiability?: boolean; - protected constructor(id: string, name: string, parentId: string, category: number, type: number, icon: string, color: string, currency: string, balance: number, comment: string, displayOrder: number, visible: boolean, balanceTime?: number, creditCardStatementDate?: number, isAsset?: boolean, isLiability?: boolean, subAccounts?: Account[]) { + protected constructor(id: string, name: string, parentId: string, category: number, type: number, icon: string, color: string, currency: string, balance: number, comment: string, displayOrder: number, visible: boolean, balanceTime?: number, lastReconciledTime?: number, creditCardStatementDate?: number, isAsset?: boolean, isLiability?: boolean, subAccounts?: Account[]) { this.id = id; this.name = name; this.parentId = parentId; @@ -35,6 +36,7 @@ export class Account implements AccountInfoResponse { this.currency = currency; this.balance = balance; this.balanceTime = balanceTime; + this.lastReconciledTime = lastReconciledTime; this.comment = comment; this.displayOrder = displayOrder; this.visible = visible; @@ -92,6 +94,7 @@ export class Account implements AccountInfoResponse { this.currency === other.currency && this.balance === other.balance && this.balanceTime === other.balanceTime && + this.lastReconciledTime === other.lastReconciledTime && this.comment === other.comment && this.displayOrder === other.displayOrder && this.visible === other.visible && @@ -128,6 +131,7 @@ export class Account implements AccountInfoResponse { this.currency = other.currency; this.balance = other.balance; this.balanceTime = other.balanceTime; + this.lastReconciledTime = other.lastReconciledTime; this.comment = other.comment; this.creditCardStatementDate = other.creditCardStatementDate; this.visible = other.visible; @@ -212,6 +216,7 @@ export class Account implements AccountInfoResponse { currency: parentAccount && (!this.id || this.id === '0') ? this.currency : undefined, balance: parentAccount && (!this.id || this.id === '0') ? this.balance : undefined, balanceTime: parentAccount && (!this.id || this.id === '0') ? this.balanceTime : undefined, + lastReconciledTime: this.lastReconciledTime, comment: this.comment, creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined, hidden: !this.visible, @@ -363,6 +368,7 @@ export class Account implements AccountInfoResponse { this.displayOrder, this.visible, this.balanceTime, + this.lastReconciledTime, this.creditCardStatementDate, this.isAsset, this.isLiability @@ -384,6 +390,7 @@ export class Account implements AccountInfoResponse { this.displayOrder, this.visible, this.balanceTime, + this.lastReconciledTime, this.creditCardStatementDate, this.isAsset, this.isLiability, @@ -405,6 +412,7 @@ export class Account implements AccountInfoResponse { 0, // displayOrder true, // visible balanceTime, // balanceTime + undefined, // lastReconciledTime 0 // creditCardStatementDate ); } @@ -424,6 +432,7 @@ export class Account implements AccountInfoResponse { 0, // displayOrder true, // visible balanceTime, // balanceTime + undefined, // lastReconciledTime 0 // creditCardStatementDate ); } @@ -443,6 +452,7 @@ export class Account implements AccountInfoResponse { accountResponse.displayOrder, !accountResponse.hidden, undefined, + accountResponse.lastReconciledTime, accountResponse.creditCardStatementDate, accountResponse.isAsset, accountResponse.isLiability, @@ -557,6 +567,7 @@ export class AccountWithDisplayBalance extends Account { account.displayOrder, account.visible, account.balanceTime, + account.lastReconciledTime, account.creditCardStatementDate, account.isAsset, account.isLiability, @@ -595,6 +606,7 @@ export interface AccountModifyRequest { readonly currency?: string; readonly balance?: number; readonly balanceTime?: number; + readonly lastReconciledTime?: number; readonly comment: string; readonly creditCardStatementDate?: number; readonly hidden: boolean; @@ -612,6 +624,7 @@ export interface AccountInfoResponse { readonly color: string; readonly currency: string; readonly balance: number; + readonly lastReconciledTime?: number; readonly comment: string; readonly creditCardStatementDate?: number; readonly displayOrder: number; diff --git a/src/models/user.ts b/src/models/user.ts index 21a3600e..4b645443 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -19,6 +19,7 @@ export class User { public firstDayOfWeek: number; public defaultAccountId: string = EMPTY_USER_BASIC_INFO.defaultAccountId; + public useLastReconciledTime: boolean = EMPTY_USER_BASIC_INFO.useLastReconciledTime; public transactionEditScope: number = EMPTY_USER_BASIC_INFO.transactionEditScope; public fiscalYearStart: number = EMPTY_USER_BASIC_INFO.fiscalYearStart; public calendarDisplayType: number = EMPTY_USER_BASIC_INFO.calendarDisplayType; @@ -48,6 +49,7 @@ export class User { this.email = user.email; this.nickname = user.nickname; this.defaultAccountId = user.defaultAccountId; + this.useLastReconciledTime = user.useLastReconciledTime; this.transactionEditScope = user.transactionEditScope; this.language = user.language; this.defaultCurrency = user.defaultCurrency; @@ -90,6 +92,7 @@ export class User { password: this.password, oldPassword: currentPassword, defaultAccountId: this.defaultAccountId, + useLastReconciledTime: this.useLastReconciledTime, transactionEditScope: this.transactionEditScope, language: this.language, defaultCurrency: this.defaultCurrency, @@ -116,6 +119,7 @@ export class User { public static of(userInfo: UserBasicInfo): User { const user = new User(userInfo.language, userInfo.defaultCurrency, userInfo.firstDayOfWeek); user.defaultAccountId = userInfo.defaultAccountId; + user.useLastReconciledTime = userInfo.useLastReconciledTime; user.transactionEditScope = userInfo.transactionEditScope; user.fiscalYearStart = userInfo.fiscalYearStart; user.calendarDisplayType = userInfo.calendarDisplayType; @@ -149,6 +153,7 @@ export interface UserBasicInfo { readonly avatar: string; readonly avatarProvider?: string; readonly defaultAccountId: string; + readonly useLastReconciledTime: boolean; readonly transactionEditScope: number; readonly language: string; readonly defaultCurrency: string; @@ -205,6 +210,7 @@ export interface UserProfileUpdateRequest { readonly password?: string; readonly oldPassword?: string; readonly defaultAccountId?: string; + readonly useLastReconciledTime?: boolean; readonly transactionEditScope?: number; readonly language?: string; readonly defaultCurrency?: string; @@ -244,6 +250,7 @@ export const EMPTY_USER_BASIC_INFO: UserBasicInfo = { avatar: '', avatarProvider: undefined, defaultAccountId: '', + useLastReconciledTime: false, transactionEditScope: TransactionEditScopeType.All.type, language: '', defaultCurrency: '', diff --git a/src/stores/user.ts b/src/stores/user.ts index c8365346..0dd74c4e 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -66,6 +66,11 @@ export const useUserStore = defineStore('user', () => { return userInfo.defaultCurrency || settingsStore.localeDefaultSettings.currency; }); + const currentUserUseLastReconciledTime = computed(() => { + const userInfo = currentUserBasicInfo.value || EMPTY_USER_BASIC_INFO; + return userInfo.useLastReconciledTime ?? false; + }); + const currentUserFirstDayOfWeek = computed(() => { const userInfo = currentUserBasicInfo.value || EMPTY_USER_BASIC_INFO; return isNumber(userInfo.firstDayOfWeek) && WeekDay.valueOf(userInfo.firstDayOfWeek) ? userInfo.firstDayOfWeek as WeekDayValue : settingsStore.localeDefaultSettings.firstDayOfWeek; @@ -443,6 +448,7 @@ export const useUserStore = defineStore('user', () => { currentUserDefaultAccountId, currentUserLanguage, currentUserDefaultCurrency, + currentUserUseLastReconciledTime, currentUserFirstDayOfWeek, currentUserFiscalYearStart, currentUserCalendarDisplayType, diff --git a/src/views/base/accounts/AccountEditPageBase.ts b/src/views/base/accounts/AccountEditPageBase.ts index 23f44cf2..89a0e2bc 100644 --- a/src/views/base/accounts/AccountEditPageBase.ts +++ b/src/views/base/accounts/AccountEditPageBase.ts @@ -38,6 +38,8 @@ export function useAccountEditPageBase() { const account = ref(Account.createNewAccount(defaultAccountCategory, userStore.currentUserDefaultCurrency, getCurrentUnixTimeForNewAccount())); const subAccounts = ref([]); + const useLastReconciledTime = computed(() => userStore.currentUserUseLastReconciledTime); + const title = computed(() => { if (!editAccountId.value) { return 'Add Account'; @@ -97,12 +99,12 @@ export function useAccountEditPageBase() { return getSameDateTimeWithCurrentTimezone(parseDateTimeFromUnixTimeWithBrowserTimezone(getCurrentUnixTime())).getUnixTime(); } - function getDefaultTimezoneOffsetMinutes(account: Account): number { - if (!account.balanceTime) { - return 0; + function getDefaultTimezoneOffsetMinutes(unixTime?: number): number { + if (!unixTime) { + return getTimezoneOffsetMinutes(getCurrentUnixTime()); } - return getTimezoneOffsetMinutes(account.balanceTime); + return getTimezoneOffsetMinutes(unixTime); } function getAccountCreditCardStatementDate(statementDate?: number): string | null { @@ -132,6 +134,23 @@ export function useAccountEditPageBase() { account.balanceTime = balanceTime - (newUtcOffset - oldUtcOffset) * 60; } + function updateAccountLastReconciledTime(account: Account, lastReconciledTime: number): void { + if (!isDefined(account.lastReconciledTime)) { + account.lastReconciledTime = lastReconciledTime; + return; + } + + const oldUtcOffset = getTimezoneOffsetMinutes(account.lastReconciledTime); + const newUtcOffset = getTimezoneOffsetMinutes(lastReconciledTime); + + if (oldUtcOffset === newUtcOffset) { + account.lastReconciledTime = lastReconciledTime; + return; + } + + account.lastReconciledTime = lastReconciledTime - (newUtcOffset - oldUtcOffset) * 60; + } + function getInputEmptyProblemMessage(account: Account, isSubAccount: boolean): string | null { if (!isSubAccount && !account.category) { return 'Account category cannot be blank'; @@ -189,6 +208,7 @@ export function useAccountEditPageBase() { account, subAccounts, // computed states + useLastReconciledTime, title, saveButtonTitle, inputEmptyProblemMessage, @@ -202,6 +222,7 @@ export function useAccountEditPageBase() { getDefaultTimezoneOffsetMinutes, getAccountCreditCardStatementDate, updateAccountBalanceTime, + updateAccountLastReconciledTime, isNewAccount, addSubAccount, setAccount diff --git a/src/views/base/users/UserProfilePageBase.ts b/src/views/base/users/UserProfilePageBase.ts index 46ce37c6..eba743ab 100644 --- a/src/views/base/users/UserProfilePageBase.ts +++ b/src/views/base/users/UserProfilePageBase.ts @@ -6,10 +6,11 @@ import { useSettingsStore } from '@/stores/setting.ts'; import { useAccountsStore } from '@/stores/account.ts'; import { useOverviewStore } from '@/stores/overview.ts'; -import type { TypeAndDisplayName } from '@/core/base.ts'; +import type { TypeAndDisplayName, LocalizedSwitchOption } from '@/core/base.ts'; import { DateDisplayType } from '@/core/calendar.ts'; import { WeekDay } from '@/core/datetime.ts'; import { type LocalizedDigitGroupingType, NumeralSystem, DecimalSeparator, DigitGroupingSymbol } from '@/core/numeral.ts'; +import { TransactionEditScopeType } from '@/core/transaction.ts'; import { type UserBasicInfo, User } from '@/models/user.ts'; import { type CategorizedAccount, Account} from '@/models/account.ts'; @@ -22,6 +23,7 @@ export function useUserProfilePageBase() { tt, getDefaultCurrency, getDefaultFirstDayOfWeek, + getAllEnableDisableOptions, getAllWeekDays, getAllCalendarDisplayTypes, getAllDateDisplayTypes, @@ -85,7 +87,8 @@ export function useUserProfilePageBase() { const allCoordinateDisplayTypes = computed(() => getAllCoordinateDisplayTypes()); const allExpenseAmountColorTypes = computed(() => getAllExpenseAmountColors()); const allIncomeAmountColorTypes = computed(() => getAllIncomeAmountColors()); - const allTransactionEditScopeTypes = computed(() => getAllTransactionEditScopeTypes()); + const allTransactionEditScopeTypes = computed(() => getAllTransactionEditScopeTypes(newProfile.value.useLastReconciledTime || (TransactionEditScopeType.valueOf(newProfile.value.transactionEditScope)?.needLastReconciledTime ?? false))); + const enableDisableOptions = computed(() => getAllEnableDisableOptions()); const languageTitle = computed(() => { const languageInCurrentLanguage = tt('Language'); @@ -114,6 +117,7 @@ export function useUserProfilePageBase() { newProfile.value.email === oldProfile.value.email && newProfile.value.nickname === oldProfile.value.nickname && newProfile.value.defaultAccountId === oldProfile.value.defaultAccountId && + newProfile.value.useLastReconciledTime === oldProfile.value.useLastReconciledTime && newProfile.value.transactionEditScope === oldProfile.value.transactionEditScope && newProfile.value.language === oldProfile.value.language && newProfile.value.defaultCurrency === oldProfile.value.defaultCurrency && @@ -229,6 +233,7 @@ export function useUserProfilePageBase() { allExpenseAmountColorTypes, allIncomeAmountColorTypes, allTransactionEditScopeTypes, + enableDisableOptions, languageTitle, supportDigitGroupingSymbol, inputIsNotChangedProblemMessage, diff --git a/src/views/desktop/accounts/list/dialogs/EditDialog.vue b/src/views/desktop/accounts/list/dialogs/EditDialog.vue index 766ee0d1..ae9539df 100644 --- a/src/views/desktop/accounts/list/dialogs/EditDialog.vue +++ b/src/views/desktop/accounts/list/dialogs/EditDialog.vue @@ -129,7 +129,7 @@ v-model="account.creditCardStatementDate" > - - + + + + (false); const activeTab = ref('account'); const currentAccountIndex = ref(-1); +const canShowBalanceTime = computed(() => (!editAccountId.value || isNewAccount(selectedAccount.value)) && (account.value.type === AccountType.SingleAccount.type || currentAccountIndex.value >= 0)); +const canShowLastReconciledTime = computed(() => useLastReconciledTime.value && !!editAccountId.value && !isNewAccount(selectedAccount.value) && (account.value.type === AccountType.SingleAccount.type || currentAccountIndex.value >= 0)); + const selectedAccount = computed(() => { if (currentAccountIndex.value < 0) { return account.value; diff --git a/src/views/desktop/user/settings/tabs/UserBasicSettingTab.vue b/src/views/desktop/user/settings/tabs/UserBasicSettingTab.vue index 07591883..4b46108f 100644 --- a/src/views/desktop/user/settings/tabs/UserBasicSettingTab.vue +++ b/src/views/desktop/user/settings/tabs/UserBasicSettingTab.vue @@ -96,6 +96,27 @@ + + + + + + + + @@ -463,27 +489,53 @@ + + @@ -542,6 +594,7 @@ import { isDefined, findDisplayNameByType } from '@/lib/common.ts'; import { generateRandomUUID } from '@/lib/misc.ts'; import { getTimezoneOffsetMinutes, + getCurrentUnixTime, parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts'; @@ -552,7 +605,9 @@ interface AccountContext { showCreditCardStatementDatePopup: boolean; showBalanceSheet: boolean; showBalanceDateTimeSheet: boolean; + showLastReconciledTimeSheet: boolean; balanceDateTimeSheetMode: string; + lastReconciledDateTimeSheetMode: string; } const props = defineProps<{ @@ -578,6 +633,7 @@ const { submitting, account, subAccounts, + useLastReconciledTime, title, inputEmptyProblemMessage, inputIsEmpty, @@ -588,6 +644,7 @@ const { getDefaultTimezoneOffsetMinutes, getAccountCreditCardStatementDate, updateAccountBalanceTime, + updateAccountLastReconciledTime, isNewAccount, addSubAccount, setAccount @@ -602,7 +659,9 @@ const DEFAULT_ACCOUNT_CONTEXT: AccountContext = { showCreditCardStatementDatePopup: false, showBalanceSheet: false, showBalanceDateTimeSheet: false, - balanceDateTimeSheetMode: 'time' + showLastReconciledTimeSheet: false, + balanceDateTimeSheetMode: 'time', + lastReconciledDateTimeSheetMode: 'time' }; const accountContext = ref(Object.assign({}, DEFAULT_ACCOUNT_CONTEXT)); @@ -621,21 +680,21 @@ function formatAccountDisplayBalance(selectedAccount: Account): string { return formatAmountToLocalizedNumeralsWithCurrency(balance, selectedAccount.currency); } -function formatAccountBalanceDate(account: Account): string { - if (!isDefined(account.balanceTime)) { +function formatDate(unixTime?: number): string { + if (!isDefined(unixTime)) { return ''; } - const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(account.balanceTime, getTimezoneOffsetMinutes(account.balanceTime)); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(unixTime, getTimezoneOffsetMinutes(unixTime)); return formatDateTimeToLongDate(dateTime); } -function formatAccountBalanceTime(account: Account): string { - if (!isDefined(account.balanceTime)) { +function formatTime(unixTime?: number): string { + if (!isDefined(unixTime)) { return ''; } - const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(account.balanceTime, getTimezoneOffsetMinutes(account.balanceTime)); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(unixTime, getTimezoneOffsetMinutes(unixTime)); return formatDateTimeToLongTime(dateTime); } @@ -739,11 +798,16 @@ function removeSubAccount(currentSubAccount: Account | null, confirm: boolean): } } -function showDateTimeDialog(accountContext: AccountContext, sheetMode: string): void { +function showBalanceDateTimeDialog(accountContext: AccountContext, sheetMode: string): void { accountContext.balanceDateTimeSheetMode = sheetMode; accountContext.showBalanceDateTimeSheet = true; } +function showLastReconciledDateTimeDialog(accountContext: AccountContext, sheetMode: string): void { + accountContext.lastReconciledDateTimeSheetMode = sheetMode; + accountContext.showLastReconciledTimeSheet = true; +} + function onPageAfterIn(): void { routeBackOnError(props.f7router, loadingError); } @@ -758,22 +822,21 @@ init();