From 5cb7eca3403d759a7ac4be76221c131f5eb8bc74 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 27 Oct 2025 00:52:41 +0800 Subject: [PATCH] add outflows / inflows / net cash flow in statistics & analysis --- pkg/api/transactions.go | 14 ++- pkg/models/transaction.go | 28 +++++- pkg/services/transactions.go | 50 +++++++---- src/components/desktop/MonthlyTrendsChart.vue | 4 +- src/core/statistics.ts | 7 +- src/core/transaction.ts | 5 ++ src/locales/de.json | 2 + src/locales/en.json | 2 + src/locales/es.json | 2 + src/locales/fr.json | 2 + src/locales/it.json | 2 + src/locales/ja.json | 2 + src/locales/ko.json | 2 + src/locales/nl.json | 2 + src/locales/pt_BR.json | 2 + src/locales/ru.json | 2 + src/locales/th.json | 2 + src/locales/uk.json | 2 + src/locales/vi.json | 2 + src/locales/zh_Hans.json | 2 + src/locales/zh_Hant.json | 2 + src/models/transaction.ts | 2 + src/stores/statistics.ts | 87 ++++++++++++++++--- .../StatisticsTransactionPageBase.ts | 20 +++-- .../desktop/statistics/TransactionPage.vue | 17 ++-- .../mobile/statistics/TransactionPage.vue | 11 ++- 26 files changed, 226 insertions(+), 49 deletions(-) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 4f0c6147..fa82fb73 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -426,7 +426,7 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, } uid := c.GetCurrentUid() - totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone) + totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone) if err != nil { log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error()) @@ -447,6 +447,11 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, AccountId: totalAmountItem.AccountId, TotalAmount: totalAmountItem.Amount, } + + if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + statisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId + statisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType() + } } return statisticResp, nil @@ -489,7 +494,7 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) } uid := c.GetCurrentUid() - allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone) + allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone) if err != nil { log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error()) @@ -512,6 +517,11 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext) AccountId: totalAmountItem.AccountId, TotalAmount: totalAmountItem.Amount, } + + if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + monthlyStatisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId + monthlyStatisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType() + } } statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp) diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index ce678e56..5409a853 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -37,6 +37,15 @@ func (t TransactionType) ToTransactionDbType() (TransactionDbType, error) { } } +// TransactionRelatedAccountType represents related account type in transaction +type TransactionRelatedAccountType byte + +// Transaction relation types +const ( + TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_FROM TransactionRelatedAccountType = 1 + TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_TO TransactionRelatedAccountType = 2 +) + // TransactionDbType represents transaction type in database type TransactionDbType byte @@ -84,6 +93,17 @@ func (t TransactionDbType) ToTransactionType() (TransactionType, error) { } } +// ToTransactionRelatedAccountType returns the related account type for this db enum +func (t TransactionDbType) ToTransactionRelatedAccountType() (TransactionRelatedAccountType, error) { + if t == TRANSACTION_DB_TYPE_TRANSFER_OUT { + return TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_TO, nil + } else if t == TRANSACTION_DB_TYPE_TRANSFER_IN { + return TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_FROM, nil + } else { + return 0, errs.ErrTransactionTypeInvalid + } +} + // TransactionTagFilterType represents transaction tag filter type type TransactionTagFilterType byte @@ -369,9 +389,11 @@ type TransactionStatisticResponse struct { // TransactionStatisticResponseItem represents total amount item for a response type TransactionStatisticResponseItem struct { - CategoryId int64 `json:"categoryId,string"` - AccountId int64 `json:"accountId,string"` - TotalAmount int64 `json:"amount"` + CategoryId int64 `json:"categoryId,string"` + AccountId int64 `json:"accountId,string"` + RelatedAccountId int64 `json:"relatedAccountId,string,omitempty"` + RelatedAccountType TransactionRelatedAccountType `json:"relatedAccountType,omitempty"` + TotalAmount int64 `json:"amount"` } // TransactionStatisticTrendsResponseItem represents the data within each statistic interval diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 327a1b50..1e70f711 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -1796,8 +1796,8 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c core.Context, ui return incomeAmounts, expenseAmounts, nil } -// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { +// GetAccountsAndCategoriesTotalInflowAndOutflow returns the every accounts and categories total inflows and outflows amount by specific date range +func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -1817,12 +1817,14 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor endTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(endUnixTime) } - condition := "uid=? AND deleted=? AND (type=? OR type=?)" - conditionParams := make([]any, 0, 4) + condition := "uid=? AND deleted=? AND (type=? OR type=? OR type=? OR type=?)" + conditionParams := make([]any, 0, 6) conditionParams = append(conditionParams, uid) conditionParams = append(conditionParams, false) conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME) conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN) minTransactionTime := startTransactionTime maxTransactionTime := endTransactionTime @@ -1850,7 +1852,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%") } - sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) + sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) @@ -1886,13 +1888,20 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor } groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId) + + if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + groupKey = fmt.Sprintf("%d_%d_%d_%d", transaction.CategoryId, transaction.AccountId, transaction.RelatedAccountId, transaction.Type) + } + totalAmounts, exists := transactionTotalAmountsMap[groupKey] if !exists { totalAmounts = &models.Transaction{ - CategoryId: transaction.CategoryId, - AccountId: transaction.AccountId, - Amount: 0, + Type: transaction.Type, + CategoryId: transaction.CategoryId, + AccountId: transaction.AccountId, + RelatedAccountId: transaction.RelatedAccountId, + Amount: 0, } transactionTotalAmountsMap[groupKey] = totalAmounts @@ -1910,8 +1919,8 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor return transactionTotalAmounts, nil } -// GetAccountsAndCategoriesMonthlyIncomeAndExpense returns the every accounts monthly income and expense amount by specific date range -func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { +// GetAccountsAndCategoriesMonthlyInflowAndOutflow returns the every accounts monthly inflows and outflows amount by specific date range +func (s *TransactionService) GetAccountsAndCategoriesMonthlyInflowAndOutflow(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -1936,12 +1945,14 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c } } - condition := "uid=? AND deleted=? AND (type=? OR type=?)" - conditionParams := make([]any, 0, 4) + condition := "uid=? AND deleted=? AND (type=? OR type=? OR type=? OR type=?)" + conditionParams := make([]any, 0, 6) conditionParams = append(conditionParams, uid) conditionParams = append(conditionParams, false) conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME) conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT) + conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN) minTransactionTime := startTransactionTime maxTransactionTime := endTransactionTime @@ -1969,7 +1980,7 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%") } - sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) + sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...) sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType) err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions) @@ -2008,13 +2019,22 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c } groupKey := fmt.Sprintf("%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId) + + if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + groupKey = fmt.Sprintf("%d_%d_%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId, transaction.RelatedAccountId, transaction.Type) + } + transactionAmounts, exists := transactionsMonthlyAmountsMap[groupKey] if !exists { transactionAmounts = &models.Transaction{ - CategoryId: transaction.CategoryId, - AccountId: transaction.AccountId, + Type: transaction.Type, + CategoryId: transaction.CategoryId, + AccountId: transaction.AccountId, + RelatedAccountId: transaction.RelatedAccountId, + Amount: 0, } + transactionsMonthlyAmountsMap[groupKey] = transactionAmounts } diff --git a/src/components/desktop/MonthlyTrendsChart.vue b/src/components/desktop/MonthlyTrendsChart.vue index 43e470e7..3b753738 100644 --- a/src/components/desktop/MonthlyTrendsChart.vue +++ b/src/components/desktop/MonthlyTrendsChart.vue @@ -482,13 +482,13 @@ defineExpose({ diff --git a/src/core/statistics.ts b/src/core/statistics.ts index 0be0ecd8..fd9805b9 100644 --- a/src/core/statistics.ts +++ b/src/core/statistics.ts @@ -92,17 +92,22 @@ export class ChartDataType implements TypeAndName { private static readonly allInstances: ChartDataType[] = []; private static readonly allInstancesByType: Record = {}; + public static readonly OutflowsByAccount = new ChartDataType(11, 'Outflows By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly ExpenseByAccount = new ChartDataType(0, 'Expense By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly ExpenseByPrimaryCategory = new ChartDataType(1, 'Expense By Primary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly ExpenseBySecondaryCategory = new ChartDataType(2, 'Expense By Secondary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); + public static readonly InflowsByAccount = new ChartDataType(12, 'Inflows By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly IncomeByAccount = new ChartDataType(3, 'Income By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly IncomeByPrimaryCategory = new ChartDataType(4, 'Income By Primary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly IncomeBySecondaryCategory = new ChartDataType(5, 'Income By Secondary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis); public static readonly AccountTotalAssets = new ChartDataType(6, 'Account Total Assets', StatisticsAnalysisType.CategoricalAnalysis); public static readonly AccountTotalLiabilities = new ChartDataType(7, 'Account Total Liabilities', StatisticsAnalysisType.CategoricalAnalysis); + public static readonly TotalOutflows = new ChartDataType(13, 'Total Outflows', StatisticsAnalysisType.TrendAnalysis); public static readonly TotalExpense = new ChartDataType(8, 'Total Expense', StatisticsAnalysisType.TrendAnalysis); + public static readonly TotalInflows = new ChartDataType(14, 'Total Inflows', StatisticsAnalysisType.TrendAnalysis); public static readonly TotalIncome = new ChartDataType(9, 'Total Income', StatisticsAnalysisType.TrendAnalysis); - public static readonly TotalBalance = new ChartDataType(10, 'Net Income', StatisticsAnalysisType.TrendAnalysis); + public static readonly NetCashFlow = new ChartDataType(15, 'Net Cash Flow', StatisticsAnalysisType.TrendAnalysis); + public static readonly NetIncome = new ChartDataType(10, 'Net Income', StatisticsAnalysisType.TrendAnalysis); public static readonly Default = ChartDataType.ExpenseByPrimaryCategory; diff --git a/src/core/transaction.ts b/src/core/transaction.ts index ae16c3e7..665b459e 100644 --- a/src/core/transaction.ts +++ b/src/core/transaction.ts @@ -7,6 +7,11 @@ export enum TransactionType { Transfer = 4 } +export enum TransactionRelatedAccountType { + TransferFrom = 1, + TransferTo = 2 +} + export class TransactionEditScopeType implements TypeAndName { private static readonly allInstances: TransactionEditScopeType[] = []; diff --git a/src/locales/de.json b/src/locales/de.json index 92dcad0a..435eb86b 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Ausgaben nach Konto", "Expense By Primary Category": "Ausgaben nach Primärkategorie", "Expense By Secondary Category": "Ausgaben nach Sekundärkategorie", + "Inflows By Account": "Inflows By Account", "Income By Account": "Einnahmen nach Konto", "Income By Primary Category": "Einnahmen nach Primärkategorie", "Income By Secondary Category": "Einnahmen nach Sekundärkategorie", diff --git a/src/locales/en.json b/src/locales/en.json index c1059cab..93477428 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Expense By Account", "Expense By Primary Category": "Expense By Primary Category", "Expense By Secondary Category": "Expense By Secondary Category", + "Inflows By Account": "Inflows By Account", "Income By Account": "Income By Account", "Income By Primary Category": "Income By Primary Category", "Income By Secondary Category": "Income By Secondary Category", diff --git a/src/locales/es.json b/src/locales/es.json index d3521d58..96f7b2b4 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Gasto por cuenta", "Expense By Primary Category": "Gasto por categoría primaria", "Expense By Secondary Category": "Gasto por categoría secundaria", + "Inflows By Account": "Inflows By Account", "Income By Account": "Ingresos por cuenta", "Income By Primary Category": "Ingresos por categoría primaria", "Income By Secondary Category": "Ingresos por categoría secundaria", diff --git a/src/locales/fr.json b/src/locales/fr.json index 0310cb46..f38dfd33 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Solde maximum", "Median Balance": "Solde médian", "Average Balance": "Solde moyen", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Dépenses par compte", "Expense By Primary Category": "Dépenses par catégorie principale", "Expense By Secondary Category": "Dépenses par catégorie secondaire", + "Inflows By Account": "Inflows By Account", "Income By Account": "Revenus par compte", "Income By Primary Category": "Revenus par catégorie principale", "Income By Secondary Category": "Revenus par catégorie secondaire", diff --git a/src/locales/it.json b/src/locales/it.json index 5e364320..69b8e7d0 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Spesa per conto", "Expense By Primary Category": "Spesa per categoria principale", "Expense By Secondary Category": "Spesa per categoria secondaria", + "Inflows By Account": "Inflows By Account", "Income By Account": "Entrata per conto", "Income By Primary Category": "Entrata per categoria principale", "Income By Secondary Category": "Entrata per categoria secondaria", diff --git a/src/locales/ja.json b/src/locales/ja.json index 3bf4e355..909997a8 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "口座別の支出", "Expense By Primary Category": "一次カテゴリ別の支出", "Expense By Secondary Category": "二次カテゴリ別の支出", + "Inflows By Account": "Inflows By Account", "Income By Account": "口座別の収入", "Income By Primary Category": "一次カテゴリ別の収入", "Income By Secondary Category": "二次カテゴリ別の収入", diff --git a/src/locales/ko.json b/src/locales/ko.json index 937b0d3d..c921819b 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "최대 잔액", "Median Balance": "중앙값 잔액", "Average Balance": "평균 잔액", + "Outflows By Account": "Outflows By Account", "Expense By Account": "계좌별 비용", "Expense By Primary Category": "주요 범주별 비용", "Expense By Secondary Category": "보조 범주별 비용", + "Inflows By Account": "Inflows By Account", "Income By Account": "계좌별 수입", "Income By Primary Category": "주요 범주별 수입", "Income By Secondary Category": "보조 범주별 수입", diff --git a/src/locales/nl.json b/src/locales/nl.json index 9952d6c5..df88f923 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximumsaldo", "Median Balance": "Mediaansaldo", "Average Balance": "Gemiddeld saldo", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Uitgaven per rekening", "Expense By Primary Category": "Uitgaven per primaire categorie", "Expense By Secondary Category": "Uitgaven per secundaire categorie", + "Inflows By Account": "Inflows By Account", "Income By Account": "Inkomsten per rekening", "Income By Primary Category": "Inkomsten per primaire categorie", "Income By Secondary Category": "Inkomsten per secundaire categorie", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index d52e87a7..d1c8e18e 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Despesa por Conta", "Expense By Primary Category": "Despesa por Categoria Primária", "Expense By Secondary Category": "Despesa por Categoria Secundária", + "Inflows By Account": "Inflows By Account", "Income By Account": "Renda por Conta", "Income By Primary Category": "Renda por Categoria Primária", "Income By Secondary Category": "Renda por Categoria Secundária", diff --git a/src/locales/ru.json b/src/locales/ru.json index 2b21e1f8..029c5002 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Расходы по счетам", "Expense By Primary Category": "Расходы по основной категории", "Expense By Secondary Category": "Расходы по вторичной категории", + "Inflows By Account": "Inflows By Account", "Income By Account": "Доходы по счетам", "Income By Primary Category": "Доходы по основной категории", "Income By Secondary Category": "Доходы по вторичной категории", diff --git a/src/locales/th.json b/src/locales/th.json index 2958925f..8d6667f6 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "ยอดสูงสุด", "Median Balance": "ยอดกลาง", "Average Balance": "ยอดเฉลี่ย", + "Outflows By Account": "Outflows By Account", "Expense By Account": "ค่าใช้จ่ายตามบัญชี", "Expense By Primary Category": "ค่าใช้จ่ายตามหมวดหลัก", "Expense By Secondary Category": "ค่าใช้จ่ายตามหมวดย่อย", + "Inflows By Account": "Inflows By Account", "Income By Account": "รายได้ตามบัญชี", "Income By Primary Category": "รายได้ตามหมวดหลัก", "Income By Secondary Category": "รายได้ตามหมวดย่อย", diff --git a/src/locales/uk.json b/src/locales/uk.json index 4e76be2d..b375b507 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Витрати за рахунками", "Expense By Primary Category": "Витрати за основними категоріями", "Expense By Secondary Category": "Витрати за другорядними категоріями", + "Inflows By Account": "Inflows By Account", "Income By Account": "Доходи за рахунками", "Income By Primary Category": "Доходи за основними категоріями", "Income By Secondary Category": "Доходи за другорядними категоріями", diff --git a/src/locales/vi.json b/src/locales/vi.json index 6b100f96..36a4d403 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "Maximum Balance", "Median Balance": "Median Balance", "Average Balance": "Average Balance", + "Outflows By Account": "Outflows By Account", "Expense By Account": "Chi phí theo tài khoản", "Expense By Primary Category": "Chi phí theo danh mục chính", "Expense By Secondary Category": "Chi phí theo danh mục phụ", + "Inflows By Account": "Inflows By Account", "Income By Account": "Thu nhập theo tài khoản", "Income By Primary Category": "Thu nhập theo danh mục chính", "Income By Secondary Category": "Thu nhập theo danh mục phụ", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 356f3655..69238aa5 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "最大余额", "Median Balance": "中位数余额", "Average Balance": "平均余额", + "Outflows By Account": "账户流出", "Expense By Account": "账户支出", "Expense By Primary Category": "一级分类支出", "Expense By Secondary Category": "二级分类支出", + "Inflows By Account": "账户流入", "Income By Account": "账户收入", "Income By Primary Category": "一级分类收入", "Income By Secondary Category": "二级分类收入", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 36476037..b696e262 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1987,9 +1987,11 @@ "Maximum Balance": "最大餘額", "Median Balance": "中位數餘額", "Average Balance": "平均餘額", + "Outflows By Account": "帳戶流出", "Expense By Account": "帳戶支出", "Expense By Primary Category": "一級分類支出", "Expense By Secondary Category": "二級分類支出", + "Inflows By Account": "帳戶流入", "Income By Account": "帳戶收入", "Income By Primary Category": "一級分類收入", "Income By Secondary Category": "二級分類收入", diff --git a/src/models/transaction.ts b/src/models/transaction.ts index a74db6d8..905856e3 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -699,6 +699,8 @@ export interface TransactionStatisticResponse { export interface TransactionStatisticResponseItem { readonly categoryId: string; readonly accountId: string; + readonly relatedAccountId?: string; + readonly relatedAccountType?: number; readonly amount: number; } diff --git a/src/stores/statistics.ts b/src/stores/statistics.ts index d174812a..39d7ab27 100644 --- a/src/stores/statistics.ts +++ b/src/stores/statistics.ts @@ -11,7 +11,10 @@ import { entries, values } from '@/core/base.ts'; import { type TextualYearMonth, type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { CategoryType } from '@/core/category.ts'; -import { TransactionTagFilterType } from '@/core/transaction.ts'; +import { + TransactionRelatedAccountType, + TransactionTagFilterType +} from '@/core/transaction.ts'; import { StatisticsAnalysisType, CategoricalChartType, @@ -61,9 +64,13 @@ import services from '@/lib/services.ts'; interface TransactionStatisticResponseItemWithInfo extends TransactionStatisticResponseItem { categoryId: string; accountId: string; + relatedAccountId?: string; amount: number; account?: Account; primaryAccount?: Account; + relatedAccount?: Account; + relatedPrimaryAccount?: Account; + relatedAccountType?: number; category?: TransactionCategory; primaryCategory?: TransactionCategory; amountInDefaultCurrency: number | null; @@ -177,7 +184,9 @@ export const useStatisticsStore = defineStore('statistics', () => { const transactionStatisticsStateInvalid = ref(true); const categoricalAnalysisChartDataCategory = computed(() => { - if (transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || + if (transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) { @@ -296,9 +305,11 @@ export const useStatisticsStore = defineStore('statistics', () => { const categoricalAnalysisData = computed(() => { let combinedData: WritableTransactionCategoricalAnalysisData | null = null; - if (transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || + if (transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type || + transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type) { @@ -434,6 +445,8 @@ export const useStatisticsStore = defineStore('statistics', () => { const item: TransactionStatisticResponseItemWithInfo = { categoryId: dataItem.categoryId, accountId: dataItem.accountId, + relatedAccountId: dataItem.relatedAccountId, + relatedAccountType: dataItem.relatedAccountType, amount: dataItem.amount, amountInDefaultCurrency: null }; @@ -448,6 +461,16 @@ export const useStatisticsStore = defineStore('statistics', () => { item.primaryAccount = item.account; } + if (item.relatedAccountId) { + item.relatedAccount = accountsStore.allAccountsMap[item.relatedAccountId]; + } + + if (item.relatedAccount && item.relatedAccount.parentId !== '0') { + item.relatedPrimaryAccount = accountsStore.allAccountsMap[item.relatedAccount.parentId]; + } else { + item.relatedPrimaryAccount = item.relatedAccount; + } + if (item.categoryId) { item.category = transactionCategoriesStore.allTransactionCategoriesMap[item.categoryId]; } @@ -486,13 +509,31 @@ export const useStatisticsStore = defineStore('statistics', () => { continue; } - if (transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseByAccount.type || + if (transactionStatisticsFilter.chartDataType === ChartDataType.OutflowsByAccount.type || + transactionStatisticsFilter.chartDataType === ChartDataType.TotalOutflows.type) { + if (item.category.type === CategoryType.Transfer) { + if (item.relatedAccountType !== TransactionRelatedAccountType.TransferTo) { + continue; + } + } else if (item.category.type !== CategoryType.Expense) { + continue; + } + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseByAccount.type || transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type || transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type || transactionStatisticsFilter.chartDataType === ChartDataType.TotalExpense.type) { if (item.category.type !== CategoryType.Expense) { continue; } + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.InflowsByAccount.type || + transactionStatisticsFilter.chartDataType === ChartDataType.TotalInflows.type) { + if (item.category.type === CategoryType.Transfer) { + if (item.relatedAccountType !== TransactionRelatedAccountType.TransferFrom) { + continue; + } + } else if (item.category.type !== CategoryType.Income) { + continue; + } } else if (transactionStatisticsFilter.chartDataType === ChartDataType.IncomeByAccount.type || transactionStatisticsFilter.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || transactionStatisticsFilter.chartDataType === ChartDataType.IncomeBySecondaryCategory.type || @@ -500,8 +541,12 @@ export const useStatisticsStore = defineStore('statistics', () => { if (item.category.type !== CategoryType.Income) { continue; } - } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalBalance.type) { + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.NetCashFlow.type) { // Do Nothing + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.NetIncome.type) { + if (item.category.type === CategoryType.Transfer) { + continue; + } } else { continue; } @@ -514,7 +559,9 @@ export const useStatisticsStore = defineStore('statistics', () => { continue; } - if (transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseByAccount.type || + if (transactionStatisticsFilter.chartDataType === ChartDataType.OutflowsByAccount.type || + transactionStatisticsFilter.chartDataType === ChartDataType.ExpenseByAccount.type || + transactionStatisticsFilter.chartDataType === ChartDataType.InflowsByAccount.type || transactionStatisticsFilter.chartDataType === ChartDataType.IncomeByAccount.type) { if (isNumber(item.amountInDefaultCurrency)) { let data = allDataItems[item.account.id]; @@ -598,14 +645,20 @@ export const useStatisticsStore = defineStore('statistics', () => { allDataItems[item.category.id] = data; } - } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalExpense.type || + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalOutflows.type || + transactionStatisticsFilter.chartDataType === ChartDataType.TotalExpense.type || + transactionStatisticsFilter.chartDataType === ChartDataType.TotalInflows.type || transactionStatisticsFilter.chartDataType === ChartDataType.TotalIncome.type || - transactionStatisticsFilter.chartDataType === ChartDataType.TotalBalance.type) { + transactionStatisticsFilter.chartDataType === ChartDataType.NetCashFlow.type || + transactionStatisticsFilter.chartDataType === ChartDataType.NetIncome.type) { if (isNumber(item.amountInDefaultCurrency)) { let data = allDataItems['total']; let amount = item.amountInDefaultCurrency; - if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalBalance.type && + if (transactionStatisticsFilter.chartDataType === ChartDataType.NetCashFlow.type && + (item.category.type === CategoryType.Expense || (item.category.type === CategoryType.Transfer && item.relatedAccountType === TransactionRelatedAccountType.TransferTo))) { + amount = -amount; + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.NetIncome.type && item.category.type === CategoryType.Expense) { amount = -amount; } @@ -615,12 +668,18 @@ export const useStatisticsStore = defineStore('statistics', () => { } else { let name = ''; - if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalExpense.type) { + if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalOutflows.type) { + name = ChartDataType.TotalOutflows.name; + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalExpense.type) { name = ChartDataType.TotalExpense.name; + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalInflows.type) { + name = ChartDataType.TotalInflows.name; } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalIncome.type) { name = ChartDataType.TotalIncome.name; - } else if (transactionStatisticsFilter.chartDataType === ChartDataType.TotalBalance.type) { - name = ChartDataType.TotalBalance.name; + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.NetCashFlow.type) { + name = ChartDataType.NetCashFlow.name; + } else if (transactionStatisticsFilter.chartDataType === ChartDataType.NetIncome.type) { + name = ChartDataType.NetIncome.name; } data = { @@ -984,7 +1043,9 @@ export const useStatisticsStore = defineStore('statistics', () => { querys.push('type=3'); } - if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type + if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type + || transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type + || transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalAssets.type || transactionStatisticsFilter.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) { diff --git a/src/views/base/statistics/StatisticsTransactionPageBase.ts b/src/views/base/statistics/StatisticsTransactionPageBase.ts index ea197db9..c1f1bf4c 100644 --- a/src/views/base/statistics/StatisticsTransactionPageBase.ts +++ b/src/views/base/statistics/StatisticsTransactionPageBase.ts @@ -190,7 +190,11 @@ export function useStatisticsTransactionPageBase() { }); const totalAmountName = computed(() => { - if (query.value.chartDataType === ChartDataType.IncomeByAccount.type + if (query.value.chartDataType === ChartDataType.InflowsByAccount.type) { + return tt('Total Inflows'); + } else if (query.value.chartDataType === ChartDataType.OutflowsByAccount.type) { + return tt('Total Outflows'); + } else if (query.value.chartDataType === ChartDataType.IncomeByAccount.type || query.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || query.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type) { return tt('Total Income'); @@ -208,15 +212,21 @@ export function useStatisticsTransactionPageBase() { }); const showTotalAmountInTrendsChart = computed(() => { - return query.value.chartDataType !== ChartDataType.TotalExpense.type && + return query.value.chartDataType !== ChartDataType.TotalOutflows.type && + query.value.chartDataType !== ChartDataType.TotalExpense.type && + query.value.chartDataType !== ChartDataType.TotalInflows.type && query.value.chartDataType !== ChartDataType.TotalIncome.type && - query.value.chartDataType !== ChartDataType.TotalBalance.type; + query.value.chartDataType !== ChartDataType.NetCashFlow.type && + query.value.chartDataType !== ChartDataType.NetIncome.type; }); const translateNameInTrendsChart = computed(() => { - return query.value.chartDataType === ChartDataType.TotalExpense.type || + return query.value.chartDataType === ChartDataType.TotalOutflows.type || + query.value.chartDataType === ChartDataType.TotalExpense.type || + query.value.chartDataType === ChartDataType.TotalInflows.type || query.value.chartDataType === ChartDataType.TotalIncome.type || - query.value.chartDataType === ChartDataType.TotalBalance.type; + query.value.chartDataType === ChartDataType.NetCashFlow.type || + query.value.chartDataType === ChartDataType.NetIncome.type; }); const categoricalAnalysisData = computed(() => statisticsStore.categoricalAnalysisData); diff --git a/src/views/desktop/statistics/TransactionPage.vue b/src/views/desktop/statistics/TransactionPage.vue index f193369d..53fbd6e3 100644 --- a/src/views/desktop/statistics/TransactionPage.vue +++ b/src/views/desktop/statistics/TransactionPage.vue @@ -48,7 +48,7 @@ - +