diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go
index f78b5b08..9ee2065b 100644
--- a/pkg/api/transactions.go
+++ b/pkg/api/transactions.go
@@ -118,7 +118,18 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
return nil, errs.Or(err, errs.ErrOperationFailed)
}
- transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Count+1, true)
+ var totalCount int64
+
+ if transactionListReq.WithCount {
+ totalCount, err = a.transactions.GetTransactionCount(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
+
+ if err != nil {
+ log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
+ return nil, errs.Or(err, errs.ErrOperationFailed)
+ }
+ }
+
+ transactions, err := a.transactions.GetTransactionsByMaxTime(uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -149,10 +160,14 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (interface{},
transactionResps.NextTimeSequenceId = nextTimeSequenceId
}
+ if transactionListReq.WithCount {
+ transactionResps.TotalCount = &totalCount
+ }
+
return transactionResps, nil
}
-// TransactionMonthListHandler returns transaction list of current user by month
+// TransactionMonthListHandler returns all transaction list of current user by month
func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interface{}, *errs.Error) {
var transactionListReq models.TransactionListInMonthByPageRequest
err := c.ShouldBindQuery(&transactionListReq)
@@ -194,20 +209,13 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
return nil, errs.Or(err, errs.ErrOperationFailed)
}
- transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, utcOffset)
+ transactions, err := a.transactions.GetTransactionsInMonthByPage(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
- totalCount, err := a.transactions.GetMonthTransactionCount(uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, utcOffset)
-
- if err != nil {
- log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transaction count in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
- return nil, errs.Or(err, errs.ErrOperationFailed)
- }
-
transactionResult, err := a.getTransactionListResult(c, user, transactions, utcOffset, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
@@ -217,7 +225,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (interfac
transactionResps := &models.TransactionInfoPageWrapperResponse2{
Items: transactionResult,
- TotalCount: totalCount,
+ TotalCount: int64(transactionResult.Len()),
}
return transactionResps, nil
diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go
index eab98482..12054c5c 100644
--- a/pkg/models/transaction.go
+++ b/pkg/models/transaction.go
@@ -112,7 +112,9 @@ type TransactionListByMaxTimeRequest struct {
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
+ Page int32 `form:"page" binding:"min=0"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
+ WithCount bool `form:"with_count"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
@@ -126,8 +128,6 @@ type TransactionListInMonthByPageRequest struct {
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
- Page int32 `form:"page" binding:"min=0"`
- Count int32 `form:"count" binding:"min=0,max=50"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
@@ -218,6 +218,7 @@ type TransactionCountResponse struct {
type TransactionInfoPageWrapperResponse struct {
Items TransactionInfoResponseSlice `json:"items"`
NextTimeSequenceId *int64 `json:"nextTimeSequenceId,string"`
+ TotalCount *int64 `json:"totalCount,omitempty"`
}
// TransactionInfoPageWrapperResponse2 represents a response of transaction which contains items and count
diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go
index b458f617..4ce364a5 100644
--- a/pkg/services/transactions.go
+++ b/pkg/services/transactions.go
@@ -70,15 +70,21 @@ func (s *TransactionService) GetAllTransactions(uid int64, pageCount int32, noDu
// GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
- return s.GetTransactionsByMaxTime(uid, maxTransactionTime, 0, 0, nil, nil, "", count, noDuplicated)
+ return s.GetTransactionsByMaxTime(uid, maxTransactionTime, 0, 0, nil, nil, "", 1, count, false, noDuplicated)
}
// GetTransactionsByMaxTime returns transactions before given time
-func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, count int32, noDuplicated bool) ([]*models.Transaction, error) {
+func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
+ if page < 0 {
+ return nil, errs.ErrPageIndexInvalid
+ } else if page == 0 {
+ page = 1
+ }
+
if count < 1 {
return nil, errs.ErrPageCountInvalid
}
@@ -86,49 +92,54 @@ func (s *TransactionService) GetTransactionsByMaxTime(uid int64, maxTransactionT
var transactions []*models.Transaction
var err error
+ actualCount := count
+
+ if needOneMoreItem {
+ actualCount++
+ }
+
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated)
- err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(int(count), 0).OrderBy("transaction_time desc").Find(&transactions)
+ err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
return transactions, err
}
-// GetTransactionsInMonthByPage returns transactions in given year and month
-func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, utcOffset int16) ([]*models.Transaction, error) {
+// GetTransactionsInMonthByPage returns all transactions in given year and month
+func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
- if page < 0 || (count > 0 && page < 1) {
- return nil, errs.ErrPageIndexInvalid
- }
-
- if count < 0 {
- return nil, errs.ErrPageCountInvalid
- }
-
- startTime, err := utils.ParseFromLongDateTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month), utcOffset)
+ startMinUnixTime, err := utils.ParseFromLongDateTimeToMinUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
+ startMaxUnixTime, err := utils.ParseFromLongDateTimeToMaxUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
if err != nil {
return nil, errs.ErrSystemError
}
- endTime := startTime.AddDate(0, 1, 0)
+ endMaxUnixTime := startMaxUnixTime.AddDate(0, 1, 0)
- minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
- maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
+ minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startMinUnixTime.Unix())
+ maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endMaxUnixTime.Unix()) - 1
var transactions []*models.Transaction
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true)
- sess := s.UserDataDB(uid).Where(condition, conditionParams...)
+ err = s.UserDataDB(uid).Where(condition, conditionParams...).OrderBy("transaction_time desc").Find(&transactions)
- if count > 0 {
- sess = sess.Limit(int(count), int(count*(page-1)))
+ transactionsInMonth := make([]*models.Transaction, 0, len(transactions))
+
+ for i := 0; i < len(transactions); i++ {
+ transaction := transactions[i]
+ transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
+ transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
+
+ if utils.IsUnixTimeEqualsYearAndMonth(transactionUnixTime, transactionTimeZone, year, month) {
+ transactionsInMonth = append(transactionsInMonth, transaction)
+ }
}
- err = sess.OrderBy("transaction_time desc").Find(&transactions)
-
- return transactions, err
+ return transactionsInMonth, err
}
// GetTransactionByTransactionId returns a transaction model according to transaction id
diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go
index 70b15c5a..98b1f2f6 100644
--- a/pkg/utils/datetimes.go
+++ b/pkg/utils/datetimes.go
@@ -13,6 +13,8 @@ const (
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
shortDateTimeFormat = "2006-1-2 15:4:5"
yearMonthDateTimeFormat = "2006-01"
+ westernmostTimezoneUtcOffset = -720 // Etc/GMT+12 (UTC-12:00)
+ easternmostTimezoneUtcOffset = 840 // Pacific/Kiritimati (UTC+14:00)
)
// FormatUnixTimeToLongDateTimeInServerTimezone returns a textual representation of the unix time formatted by long date time format
@@ -42,6 +44,18 @@ func FormatUnixTimeToYearMonth(unixTime int64, timezone *time.Location) string {
return t.Format(yearMonthDateTimeFormat)
}
+// ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone)
+func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) {
+ timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60)
+ return time.ParseInLocation(longDateTimeFormat, t, timezone)
+}
+
+// ParseFromLongDateTimeToMaxUnixTime parses a formatted string in long date time format to maximal unix time (the easternmost timezone)
+func ParseFromLongDateTimeToMaxUnixTime(t string) (time.Time, error) {
+ timezone := time.FixedZone("Timezone", westernmostTimezoneUtcOffset*60)
+ return time.ParseInLocation(longDateTimeFormat, t, timezone)
+}
+
// ParseFromLongDateTime parses a formatted string in long date time format
func ParseFromLongDateTime(t string, utcOffset int16) (time.Time, error) {
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
@@ -59,6 +73,12 @@ func ParseFromShortDateTime(t string, utcOffset int16) (time.Time, error) {
return time.ParseInLocation(shortDateTimeFormat, t, timezone)
}
+// IsUnixTimeEqualsYearAndMonth returns whether year and month of the unix time are equals to the specified year and month
+func IsUnixTimeEqualsYearAndMonth(unixTime int64, timezone *time.Location, year int32, month int32) bool {
+ date := parseFromUnixTime(unixTime).In(timezone)
+ return date.Year() == int(year) && int(date.Month()) == int(month)
+}
+
// GetTimezoneOffsetMinutes returns offset minutes according specified timezone
func GetTimezoneOffsetMinutes(timezone *time.Location) int16 {
_, tzOffset := time.Now().In(timezone).Zone()
diff --git a/src/lib/datetime.js b/src/lib/datetime.js
index fdf14801..02ee4194 100644
--- a/src/lib/datetime.js
+++ b/src/lib/datetime.js
@@ -414,3 +414,14 @@ export function isDateRangeMatchFullMonths(minTime, maxTime) {
return firstDayOfMonth.unix() === minDateTime.unix() && lastDayOfMonth.unix() === maxDateTime.unix();
}
+
+export function isDateRangeMatchOneMonth(minTime, maxTime) {
+ const minDateTime = parseDateFromUnixTime(minTime);
+ const maxDateTime = parseDateFromUnixTime(maxTime);
+
+ if (getYear(minDateTime) !== getYear(maxDateTime) || getMonth(minDateTime) !== getMonth(maxDateTime)) {
+ return false;
+ }
+
+ return isDateRangeMatchFullMonths(minTime, maxTime);
+}
diff --git a/src/lib/services.js b/src/lib/services.js
index 08f826ad..3334c938 100644
--- a/src/lib/services.js
+++ b/src/lib/services.js
@@ -237,9 +237,9 @@ export default {
id
});
},
- getTransactions: ({ maxTime, minTime, type, categoryId, accountId, keyword }) => {
+ getTransactions: ({ maxTime, minTime, count, page, withCount, type, categoryId, accountId, keyword }) => {
keyword = encodeURIComponent(keyword);
- return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&count=50&trim_account=true&trim_category=true&trim_tag=true`);
+ return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&count=${count}&page=${page}&with_count=${withCount}&trim_account=true&trim_category=true&trim_tag=true`);
},
getAllTransactionsByMonth: ({ year, month, type, categoryId, accountId, keyword }) => {
keyword = encodeURIComponent(keyword);
diff --git a/src/stores/transaction.js b/src/stores/transaction.js
index aacfe3f9..9b5f337d 100644
--- a/src/stores/transaction.js
+++ b/src/stores/transaction.js
@@ -401,7 +401,7 @@ export const useTransactionsStore = defineStore('transactions', {
return querys.join('&');
},
- loadTransactions({ reload, yearMonth, autoExpand, defaultCurrency }) {
+ loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }) {
const self = this;
const settingsStore = useSettingsStore();
const exchangeRatesStore = useExchangeRatesStore();
@@ -414,29 +414,17 @@ export const useTransactionsStore = defineStore('transactions', {
}
return new Promise((resolve, reject) => {
- let promise = null;
-
- if (yearMonth) {
- promise = services.getAllTransactionsByMonth({
- year: yearMonth.year,
- month: yearMonth.month,
- type: self.transactionsFilter.type,
- categoryId: self.transactionsFilter.categoryId,
- accountId: self.transactionsFilter.accountId,
- keyword: self.transactionsFilter.keyword
- });
- } else {
- promise = services.getTransactions({
- maxTime: actualMaxTime,
- minTime: self.transactionsFilter.minTime * 1000,
- type: self.transactionsFilter.type,
- categoryId: self.transactionsFilter.categoryId,
- accountId: self.transactionsFilter.accountId,
- keyword: self.transactionsFilter.keyword
- });
- }
-
- promise.then(response => {
+ services.getTransactions({
+ maxTime: actualMaxTime,
+ minTime: self.transactionsFilter.minTime * 1000,
+ count: count || 50,
+ page: page || 1,
+ withCount: (!!withCount) || false,
+ type: self.transactionsFilter.type,
+ categoryId: self.transactionsFilter.categoryId,
+ accountId: self.transactionsFilter.accountId,
+ keyword: self.transactionsFilter.keyword
+ }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
@@ -497,6 +485,74 @@ export const useTransactionsStore = defineStore('transactions', {
});
});
},
+ loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }) {
+ const self = this;
+ const settingsStore = useSettingsStore();
+ const exchangeRatesStore = useExchangeRatesStore();
+
+ return new Promise((resolve, reject) => {
+ services.getAllTransactionsByMonth({
+ year: year,
+ month: month,
+ type: self.transactionsFilter.type,
+ categoryId: self.transactionsFilter.categoryId,
+ accountId: self.transactionsFilter.accountId,
+ keyword: self.transactionsFilter.keyword
+ }).then(response => {
+ const data = response.data;
+
+ if (!data || !data.success || !data.result) {
+ loadTransactionList(self, settingsStore, exchangeRatesStore, {
+ transactions: emptyTransactionResult,
+ reload: true,
+ autoExpand: autoExpand,
+ defaultCurrency: defaultCurrency
+ });
+
+ if (!self.transactionListStateInvalid) {
+ self.updateTransactionListInvalidState(true);
+ }
+
+ reject({ message: 'Unable to get transaction list' });
+ return;
+ }
+
+ loadTransactionList(self, settingsStore, exchangeRatesStore, {
+ transactions: data.result,
+ reload: true,
+ autoExpand: autoExpand,
+ defaultCurrency: defaultCurrency
+ });
+
+ if (self.transactionListStateInvalid) {
+ self.updateTransactionListInvalidState(false);
+ }
+
+ resolve(data.result);
+ }).catch(error => {
+ logger.error('failed to load monthly all transaction list', error);
+
+ loadTransactionList(self, settingsStore, exchangeRatesStore, {
+ transactions: emptyTransactionResult,
+ reload: true,
+ autoExpand: autoExpand,
+ defaultCurrency: defaultCurrency
+ });
+
+ if (!self.transactionListStateInvalid) {
+ self.updateTransactionListInvalidState(true);
+ }
+
+ if (error.response && error.response.data && error.response.data.errorMessage) {
+ reject({ error: error.response.data });
+ } else if (!error.processed) {
+ reject({ message: 'Unable to get transaction list' });
+ } else {
+ reject(error);
+ }
+ });
+ });
+ },
getTransaction({ transactionId }) {
return new Promise((resolve, reject) => {
services.getTransaction({
diff --git a/src/views/desktop/TransactionsPage.vue b/src/views/desktop/TransactionsPage.vue
index 753b32c1..e785c684 100644
--- a/src/views/desktop/TransactionsPage.vue
+++ b/src/views/desktop/TransactionsPage.vue
@@ -79,20 +79,20 @@
{{ `${queryMinTime} - ${queryMaxTime}` }}