From 5a95ef07dfad4e5726c186bf99b42a6df7e7b9ec Mon Sep 17 00:00:00 2001 From: MaysWind Date: Thu, 14 Jan 2021 01:03:06 +0800 Subject: [PATCH] transaction overview supports multi currencies --- pkg/api/overviews.go | 71 +++++++++++++++++++++++++++++++++--- pkg/models/overview.go | 14 +++++-- pkg/services/transactions.go | 20 +++++----- src/store/overview.js | 57 +++++++++++++++++++++++++++-- src/views/mobile/Home.vue | 43 ++++++++++++---------- 5 files changed, 163 insertions(+), 42 deletions(-) diff --git a/pkg/api/overviews.go b/pkg/api/overviews.go index 74eb1199..5fa832f9 100644 --- a/pkg/api/overviews.go +++ b/pkg/api/overviews.go @@ -14,12 +14,14 @@ import ( // OverviewApi represents overview api type OverviewApi struct { transactions *services.TransactionService + accounts *services.AccountService } // Initialize an overview api singleton instance var ( Overviews = &OverviewApi{ transactions: services.Transactions, + accounts: services.Accounts, } ) @@ -79,23 +81,82 @@ func (a *OverviewApi) TransactionOverviewHandler(c *core.Context) (interface{}, uid := c.GetCurrentUid() + accounts, err := a.accounts.GetAllAccountsByUid(uid) + accountMap := a.accounts.GetAccountMapByList(accounts) + + if err != nil { + log.ErrorfWithRequestId(c, "[overviews.TransactionOverviewHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + overviewResp := make(map[string]*models.TransactionOverviewResponseItem) for i := 0; i < len(requestItems); i++ { requestItem := requestItems[i] - incomeAmount, expenseAmount, err := a.transactions.GetTotalIncomeAndExpenseByDateRange(uid, requestItem.StartTime, requestItem.EndTime) + incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(uid, requestItem.StartTime, requestItem.EndTime) if err != nil { log.ErrorfWithRequestId(c, "[overviews.TransactionOverviewHandler] failed to get transaction overview item for user \"uid:%d\", because %s", uid, err.Error()) return nil, errs.ErrOperationFailed } + amountsMap := make(map[string]*models.TransactionOverviewResponseItemAmount) + + for accountId, incomeAmount := range incomeAmounts { + account, exists := accountMap[accountId] + + if !exists { + log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) + continue + } + + totalAmounts, exists := amountsMap[account.Currency] + + if !exists { + totalAmounts = &models.TransactionOverviewResponseItemAmount{ + Currency: account.Currency, + IncomeAmount: 0, + ExpenseAmount: 0, + } + } + + totalAmounts.IncomeAmount += incomeAmount + amountsMap[account.Currency] = totalAmounts + } + + for accountId, expenseAmount := range expenseAmounts { + account, exists := accountMap[accountId] + + if !exists { + log.WarnfWithRequestId(c, "[overviews.TransactionOverviewHandler] cannot find account for account \"id:%d\" of user \"uid:%d\", because %s", accountId, uid) + continue + } + + totalAmounts, exists := amountsMap[account.Currency] + + if !exists { + totalAmounts = &models.TransactionOverviewResponseItemAmount{ + Currency: account.Currency, + IncomeAmount: 0, + ExpenseAmount: 0, + } + } + + totalAmounts.ExpenseAmount += expenseAmount + amountsMap[account.Currency] = totalAmounts + } + + allTotalAmounts := make([]*models.TransactionOverviewResponseItemAmount, 0) + + for _, totalAmounts := range amountsMap { + allTotalAmounts = append(allTotalAmounts, totalAmounts) + } + overviewResp[requestItem.Name] = &models.TransactionOverviewResponseItem{ - StartTime: requestItem.StartTime, - EndTime: requestItem.EndTime, - IncomeAmount: incomeAmount, - ExpenseAmount: expenseAmount, + StartTime: requestItem.StartTime, + EndTime: requestItem.EndTime, + Amounts: allTotalAmounts, } } diff --git a/pkg/models/overview.go b/pkg/models/overview.go index 48e62d3c..d945a8ef 100644 --- a/pkg/models/overview.go +++ b/pkg/models/overview.go @@ -14,8 +14,14 @@ type TransactionOverviewRequestItem struct { // TransactionOverviewResponseItem represents an item of transaction overview type TransactionOverviewResponseItem struct { - StartTime int64 `json:"startTime"` - EndTime int64 `json:"endTime"` - IncomeAmount int64 `json:"incomeAmount"` - ExpenseAmount int64 `json:"expenseAmount"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + Amounts []*TransactionOverviewResponseItemAmount `json:"amounts"` +} + +// TransactionOverviewResponseItemAmount represents amount info for an response item +type TransactionOverviewResponseItemAmount struct { + Currency string `json:"currency"` + IncomeAmount int64 `json:"incomeAmount"` + ExpenseAmount int64 `json:"expenseAmount"` } diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index e87586a3..5bfacf06 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -923,36 +923,36 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction * return relatedTransaction } -// GetTotalIncomeAndExpenseByDateRange returns the total income and expense amount by specific date range -func (s *TransactionService) GetTotalIncomeAndExpenseByDateRange(uid int64, startUnixTime int64, endUnixTime int64) (int64, int64, error) { +// GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range +func (s *TransactionService) GetAccountsTotalIncomeAndExpense(uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) { if uid <= 0 { - return 0, 0, errs.ErrUserIdInvalid + return nil, nil, errs.ErrUserIdInvalid } startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime) endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime) var transactionTotalAmounts []*models.Transaction - err := s.UserDataDB(uid).Select("uid, type, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type").Find(&transactionTotalAmounts) + err := s.UserDataDB(uid).Select("uid, type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts) if err != nil { - return 0, 0, err + return nil, nil, err } - var incomeAmount int64 - var expenseAmount int64 + incomeAmounts := make(map[int64]int64) + expenseAmounts := make(map[int64]int64) for i := 0; i < len(transactionTotalAmounts); i++ { transactionTotalAmount := transactionTotalAmounts[i] if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_INCOME { - incomeAmount = transactionTotalAmount.Amount + incomeAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount } else if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_EXPENSE { - expenseAmount = transactionTotalAmount.Amount + expenseAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount } } - return incomeAmount, expenseAmount, nil + return incomeAmounts, expenseAmounts, nil } func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error { diff --git a/src/store/overview.js b/src/store/overview.js index e6a66900..b43fa778 100644 --- a/src/store/overview.js +++ b/src/store/overview.js @@ -1,12 +1,15 @@ import services from '../lib/services.js'; import logger from '../lib/logger.js'; +import utils from '../lib/utils.js'; + +import { getExchangedAmount } from "./exchangeRates.js"; import { LOAD_TRANSACTION_OVERVIEW, UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, } from './mutations.js'; -export function loadTransactionOverview(context, { dateRange, force }) { +export function loadTransactionOverview(context, { defaultCurrency, dateRange, force }) { if (!force && !context.state.transactionOverviewStateInvalid) { return new Promise((resolve) => { resolve(context.state.transactionOverview); @@ -27,13 +30,61 @@ export function loadTransactionOverview(context, { dateRange, force }) { return; } - context.commit(LOAD_TRANSACTION_OVERVIEW, data.result); + const overview = data.result; + + for (let field in overview) { + if (!Object.prototype.hasOwnProperty.call(overview, field)) { + continue; + } + + const item = overview[field]; + + if (!item.amounts || !item.amounts.length) { + item.amounts = []; + } + + let totalIncomeAmount = 0; + let totalExpenseAmount = 0; + let hasUnCalculatedTotalIncome = false; + let hasUnCalculatedTotalExpense = false; + + for (let i = 0; i < item.amounts.length; i++) { + const amount = item.amounts[i]; + + if (amount.currency !== defaultCurrency) { + const incomeAmount = getExchangedAmount(context.state)(amount.incomeAmount, amount.currency, defaultCurrency); + const expenseAmount = getExchangedAmount(context.state)(amount.expenseAmount, amount.currency, defaultCurrency); + + if (utils.isNumber(incomeAmount)) { + totalIncomeAmount += Math.floor(incomeAmount); + } else { + hasUnCalculatedTotalIncome = true; + } + + if (utils.isNumber(expenseAmount)) { + totalExpenseAmount += Math.floor(expenseAmount); + } else { + hasUnCalculatedTotalExpense = true; + } + } else { + totalIncomeAmount += amount.incomeAmount; + totalExpenseAmount += amount.expenseAmount; + } + } + + item.incomeAmount = totalIncomeAmount; + item.expenseAmount = totalExpenseAmount; + item.incompleteIncomeAmount = hasUnCalculatedTotalIncome; + item.incompleteExpenseAmount = hasUnCalculatedTotalExpense; + } + + context.commit(LOAD_TRANSACTION_OVERVIEW, overview); if (context.state.transactionOverviewStateInvalid) { context.commit(UPDATE_TRANSACTION_OVERVIEW_INVALID_STATE, false); } - resolve(data.result); + resolve(overview); }).catch(error => { if (force) { logger.error('failed to force load transaction overview', error); diff --git a/src/views/mobile/Home.vue b/src/views/mobile/Home.vue index 91d5a679..afcdf97f 100644 --- a/src/views/mobile/Home.vue +++ b/src/views/mobile/Home.vue @@ -20,7 +20,7 @@

0.00 USD - {{ thisMonthExpense | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ thisMonthAmount.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(thisMonthAmount.incompleteExpenseAmount) }} @@ -29,7 +29,7 @@ Income of this month 0.00 USD {{ $t('Income of this month') }} - {{ thisMonthIncome | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ thisMonthAmount.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(thisMonthAmount.incompleteIncomeAmount) }}

@@ -53,11 +53,11 @@
0.00 USD - {{ transactionOverview.today.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.today.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.today.incompleteIncomeAmount) }}
0.00 USD - {{ transactionOverview.today.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.today.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.today.incompleteExpenseAmount) }}
@@ -80,11 +80,11 @@
0.00 USD - {{ transactionOverview.thisWeek.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisWeek.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisWeek.incompleteIncomeAmount) }}
0.00 USD - {{ transactionOverview.thisWeek.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisWeek.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisWeek.incompleteExpenseAmount) }}
@@ -107,11 +107,11 @@
0.00 USD - {{ transactionOverview.thisMonth.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisMonth.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisMonth.incompleteIncomeAmount) }}
0.00 USD - {{ transactionOverview.thisMonth.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisMonth.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisMonth.incompleteExpenseAmount) }}
@@ -131,11 +131,11 @@
0.00 USD - {{ transactionOverview.thisYear.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisYear.incomeAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisYear.incompleteIncomeAmount) }}
0.00 USD - {{ transactionOverview.thisYear.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) }} + {{ transactionOverview.thisYear.expenseAmount | amount(showAmountInHomePage) | currency(defaultCurrency) | completeAmount(transactionOverview.thisYear.incompleteExpenseAmount) }}
@@ -185,19 +185,17 @@ export default { defaultCurrency() { return this.$store.getters.currentUserDefaultCurrency || this.$t('default.currency'); }, - thisMonthExpense() { + thisMonthAmount() { if (!this.$store.state.transactionOverview || !this.$store.state.transactionOverview.thisMonth) { - return 0; + return { + incomeAmount : 0, + expenseAmount : 0, + incompleteIncomeAmount: false, + incompleteExpenseAmount : false + }; } - return this.$store.state.transactionOverview.thisMonth.expenseAmount; - }, - thisMonthIncome() { - if (!this.$store.state.transactionOverview || !this.$store.state.transactionOverview.thisMonth) { - return 0; - } - - return this.$store.state.transactionOverview.thisMonth.incomeAmount; + return this.$store.state.transactionOverview.thisMonth; } }, created() { @@ -206,6 +204,7 @@ export default { self.loading = true; self.$store.dispatch('loadTransactionOverview', { + defaultCurrency: self.defaultCurrency, dateRange: self.dateRange, force: false }).then(() => { @@ -237,6 +236,7 @@ export default { const self = this; self.$store.dispatch('loadTransactionOverview', { + defaultCurrency: self.defaultCurrency, dateRange: self.dateRange, force: true }).then(() => { @@ -288,6 +288,9 @@ export default { return amount; }, + completeAmount(amount, incomplete) { + return amount + (incomplete ? '+' : ''); + }, monthNameLocalizedKey(monthName) { return `datetime.${monthName}.long`; }