From 515b9af61ae776762b0b56d03f366108a52f40d1 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 21 Jul 2025 00:40:02 +0800 Subject: [PATCH] add reconciliation statement in desktop version --- cmd/webserver.go | 1 + pkg/api/transactions.go | 103 +++++++ pkg/models/transaction.go | 24 ++ pkg/services/accounts.go | 22 ++ pkg/services/transactions.go | 66 ++++ .../mobile/DateTimeSelectionSheet.vue | 2 +- src/lib/datetime.ts | 4 +- src/lib/services.ts | 5 + src/locales/de.json | 6 + src/locales/en.json | 6 + src/locales/es.json | 6 + src/locales/it.json | 6 + src/locales/ja.json | 6 + src/locales/pt_BR.json | 6 + src/locales/ru.json | 6 + src/locales/uk.json | 6 + src/locales/vi.json | 6 + src/locales/zh_Hans.json | 6 + src/locales/zh_Hant.json | 6 + src/models/transaction.ts | 14 + src/stores/transaction.ts | 31 ++ .../base/accounts/AccountListPageBase.ts | 5 + .../ReconciliationStatementPageBase.ts | 168 +++++++++++ src/views/desktop/accounts/ListPage.vue | 94 +++++- .../dialogs/ReconciliationStatementDialog.vue | 281 ++++++++++++++++++ 25 files changed, 882 insertions(+), 4 deletions(-) create mode 100644 src/views/base/transactions/ReconciliationStatementPageBase.ts create mode 100644 src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue diff --git a/cmd/webserver.go b/cmd/webserver.go index 442975a1..5944ef67 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -344,6 +344,7 @@ func startWebServer(c *core.CliContext) error { apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler)) apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler)) apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler)) + apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler)) apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler)) apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler)) apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler)) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 195db567..bacad4ab 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -22,6 +22,8 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/utils" ) +const pageCountForAccountStatement = 1000 + // TransactionsApi represents transaction api type TransactionsApi struct { ApiUsingConfig @@ -286,6 +288,107 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any, return transactionResps, nil } +// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user +func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) { + var reconciliationStatementRequest models.TransactionReconciliationStatementRequest + err := c.ShouldBindQuery(&reconciliationStatementRequest) + + if err != nil { + log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone offset, because %s", err.Error()) + return nil, errs.ErrClientTimezoneOffsetInvalid + } + + uid := c.GetCurrentUid() + user, err := a.users.GetUserById(c, uid) + + if err != nil { + if !errs.IsCustomError(err) { + log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get user, because %s", err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + account, err := a.accounts.GetAccountByAccountId(c, uid, reconciliationStatementRequest.AccountId) + + if err != nil { + log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.AccountId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + if account.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT { + log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] account \"id:%d\" for user \"uid:%d\" is not a single account", reconciliationStatementRequest.AccountId, uid) + return nil, errs.ErrAccountTypeInvalid + } + + maxTransactionTime := int64(0) + + if reconciliationStatementRequest.EndTime > 0 { + maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(reconciliationStatementRequest.EndTime) + } + + minTransactionTime := int64(0) + + if reconciliationStatementRequest.StartTime > 0 { + minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime) + } + + transactionsWithAccountBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId) + + if err != nil { + log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + transactions := make([]*models.Transaction, len(transactionsWithAccountBalance)) + transactionAccountBalanceMap := make(map[int64]*models.TransactionWithAccountBalance, len(transactionsWithAccountBalance)) + + for i := 0; i < len(transactionsWithAccountBalance); i++ { + transactionWithBalance := transactionsWithAccountBalance[i] + transactions[i] = transactionWithBalance.Transaction + transactionAccountBalanceMap[transactionWithBalance.TransactionId] = transactionWithBalance + transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance + } + + transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true) + + if err != nil { + log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + responseItems := make([]*models.TransactionReconciliationStatementResponseItem, len(transactionResult)) + + for i := 0; i < len(transactionResult); i++ { + transactionResult := transactionResult[i] + accountBalance := int64(0) + + if transactionWithBalance, exists := transactionAccountBalanceMap[transactionResult.Id]; exists { + accountBalance = transactionWithBalance.AccountBalance + } else { + log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] missing account balance for transaction \"id:%d\" of user \"uid:%d\"", transactionResult.Id, uid) + } + + responseItems[i] = &models.TransactionReconciliationStatementResponseItem{ + TransactionInfoResponse: transactionResult, + AccountBalance: accountBalance, + } + } + + reconciliationStatementResp := &models.TransactionReconciliationStatementResponse{ + Transactions: responseItems, + } + + return reconciliationStatementResp, nil +} + // TransactionStatisticsHandler returns transaction statistics of current user func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, *errs.Error) { var statisticReq models.TransactionStatisticRequest diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index b9189713..f996bf59 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -120,6 +120,12 @@ type Transaction struct { DeletedUnixTime int64 } +// TransactionWithAccountBalance represents a transaction item with account balance +type TransactionWithAccountBalance struct { + *Transaction + AccountBalance int64 +} + // TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request type TransactionGeoLocationRequest struct { Latitude float64 `json:"latitude" binding:"required"` @@ -222,6 +228,13 @@ type TransactionListInMonthByPageRequest struct { TrimTag bool `form:"trim_tag"` } +// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request +type TransactionReconciliationStatementRequest struct { + AccountId int64 `form:"account_id,string" binding:"required,min=1"` + StartTime int64 `form:"start_time"` + EndTime int64 `form:"end_time"` +} + // TransactionStatisticRequest represents all parameters of transaction statistic request type TransactionStatisticRequest struct { StartTime int64 `form:"start_time" binding:"min=0"` @@ -322,6 +335,17 @@ type TransactionInfoPageWrapperResponse2 struct { TotalCount int64 `json:"totalCount"` } +// TransactionReconciliationStatementResponseItem represents a transaction reconciliation statement response +type TransactionReconciliationStatementResponseItem struct { + *TransactionInfoResponse + AccountBalance int64 `json:"accountBalance"` +} + +// TransactionReconciliationStatementResponse represents the response of all transaction reconciliation statement response +type TransactionReconciliationStatementResponse struct { + Transactions []*TransactionReconciliationStatementResponseItem `json:"transactions"` +} + // TransactionStatisticResponse represents transaction statistic response type TransactionStatisticResponse struct { StartTime int64 `json:"startTime"` diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index e42e8549..49397e75 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -56,6 +56,28 @@ func (s *AccountService) GetAllAccountsByUid(c core.Context, uid int64) ([]*mode return accounts, err } +// GetAccountByAccountId returns account model according to account id +func (s *AccountService) GetAccountByAccountId(c core.Context, uid int64, accountId int64) (*models.Account, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if accountId <= 0 { + return nil, errs.ErrAccountIdInvalid + } + + account := &models.Account{} + has, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Get(account) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrAccountNotFound + } + + return account, err +} + // GetAccountAndSubAccountsByAccountId returns account model and sub-account models according to account id func (s *AccountService) GetAccountAndSubAccountsByAccountId(c core.Context, uid int64, accountId int64) ([]*models.Account, error) { if uid <= 0 { diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index d828da9b..097da436 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -107,6 +107,72 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int return allTransactions, nil } +// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range +func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64) ([]*models.TransactionWithAccountBalance, error) { + if maxTransactionTime <= 0 { + maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix()) + } + + var allTransactions []*models.Transaction + + for maxTransactionTime > 0 { + transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCount, false, true) + + if err != nil { + return nil, err + } + + allTransactions = append(allTransactions, transactions...) + + if len(transactions) < int(pageCount) { + maxTransactionTime = 0 + break + } + + maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1 + } + + allTransactionsAndAccountBalance := make([]*models.TransactionWithAccountBalance, 0, len(allTransactions)) + + if len(allTransactions) < 1 { + return allTransactionsAndAccountBalance, nil + } + + accumulatedBalance := int64(0) + + for i := len(allTransactions) - 1; i >= 0; i-- { + transaction := allTransactions[i] + + if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { + accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME { + accumulatedBalance = accumulatedBalance + transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE { + accumulatedBalance = accumulatedBalance - transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { + accumulatedBalance = accumulatedBalance - transaction.Amount + } else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + accumulatedBalance = accumulatedBalance + transaction.Amount + } else { + log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type) + return nil, errs.ErrTransactionTypeInvalid + } + + if transaction.TransactionTime < minTransactionTime { + continue + } + + transactionsAndAccountBalance := &models.TransactionWithAccountBalance{ + Transaction: transaction, + AccountBalance: accumulatedBalance, + } + + allTransactionsAndAccountBalance = append(allTransactionsAndAccountBalance, transactionsAndAccountBalance) + } + + return allTransactionsAndAccountBalance, nil +} + // GetTransactionsByMaxTime returns transactions before given time func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) { if uid <= 0 { diff --git a/src/components/mobile/DateTimeSelectionSheet.vue b/src/components/mobile/DateTimeSelectionSheet.vue index 9eb26196..5008a1e8 100644 --- a/src/components/mobile/DateTimeSelectionSheet.vue +++ b/src/components/mobile/DateTimeSelectionSheet.vue @@ -326,7 +326,7 @@ function getTimerPickerItemStyle(textualValue: string, textualCurrentValue: stri } } - let angle = -24 * valueDiff; + const angle = -24 * valueDiff; if (angle > 180) { return ''; diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 1c9dbff6..4e46bc4c 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -923,8 +923,8 @@ export function getFullMonthDateRange(minTime: number, maxTime: number, firstDay export function getCombinedDateAndTimeValues(date: Date, hour: string, minute: string, second: string, meridiemIndicator: string, is24Hour: boolean): Date { const newDateTime = new Date(date.valueOf()); let hours = parseInt(hour); - let minutes = parseInt(minute); - let seconds = parseInt(second); + const minutes = parseInt(minute); + const seconds = parseInt(second); if (!is24Hour) { if (hours === 12) { diff --git a/src/lib/services.ts b/src/lib/services.ts index 166a9741..3b426a1e 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -65,6 +65,8 @@ import type { TransactionInfoResponse, TransactionInfoPageWrapperResponse, TransactionInfoPageWrapperResponse2, + TransactionReconciliationStatementRequest, + TransactionReconciliationStatementResponse, TransactionStatisticRequest, TransactionStatisticResponse, TransactionStatisticTrendsRequest, @@ -413,6 +415,9 @@ export default { const keyword = encodeURIComponent(req.keyword); return axios.get>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_ids=${req.tagIds}&tag_filter_type=${req.tagFilterType}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); }, + getReconciliationStatements: (req: TransactionReconciliationStatementRequest): ApiResponsePromise => { + return axios.get>(`v1/transactions/reconciliation_statements.json?account_id=${req.accountId}&start_time=${req.startTime}&end_time=${req.endTime}`); + }, getTransactionStatistics: (req: TransactionStatisticRequest): ApiResponsePromise => { const queryParams = []; diff --git a/src/locales/de.json b/src/locales/de.json index faf4e12e..ba86e2fa 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1562,6 +1562,8 @@ "Expense": "Ausgabe", "Income": "Einkommen", "Transfer": "Überweisung", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Bargeld", "Checking Account": "Girokonto", "Credit Card": "Kreditkarte", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Konto kann nicht gelöscht werden", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Transaktion", "Transactions": "Transaktionen", "Transaction Pictures": "Transaktionsbilder", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Transaktionsbeschreibung suchen", "Unable to retrieve transaction list": "Transaktionsliste kann nicht abgerufen werden", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Benutzerdefinierter Datumsbereich", "Transaction Detail": "Transaktionsdetails", "No transaction data": "Keine Transaktionsdaten", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Gesamtverbindlichkeiten", "Total Expense": "Gesamtausgaben", "Total Income": "Gesamteinnahmen", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Gesamtsaldo", "Expense By Account": "Ausgaben nach Konto", "Expense By Primary Category": "Ausgaben nach Primärkategorie", diff --git a/src/locales/en.json b/src/locales/en.json index b63f6b1d..3cd44e8b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1562,6 +1562,8 @@ "Expense": "Expense", "Income": "Income", "Transfer": "Transfer", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Cash", "Checking Account": "Checking Account", "Credit Card": "Credit Card", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Unable to delete this account", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Transaction", "Transactions": "Transactions", "Transaction Pictures": "Transaction Pictures", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Search transaction description", "Unable to retrieve transaction list": "Unable to retrieve transaction list", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Custom Date Range", "Transaction Detail": "Transaction Detail", "No transaction data": "No transaction data", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Total Liabilities", "Total Expense": "Total Expense", "Total Income": "Total Income", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Total Balance", "Expense By Account": "Expense By Account", "Expense By Primary Category": "Expense By Primary Category", diff --git a/src/locales/es.json b/src/locales/es.json index 7c15ab38..86d39991 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1562,6 +1562,8 @@ "Expense": "Gastos", "Income": "Ingresos", "Transfer": "Transferencias", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Dinero", "Checking Account": "Cuenta de cheques", "Credit Card": "Tarjeta de crédito", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "No se puede eliminar esta cuenta", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Transacción", "Transactions": "Transacciones", "Transaction Pictures": "Imágenes de transacciones", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Buscar descripción de transacción", "Unable to retrieve transaction list": "No se puede recuperar la lista de transacciones", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Rango de fechas personalizado", "Transaction Detail": "Detalle de la transacción", "No transaction data": "Sin datos de transacción", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Pasivos totales", "Total Expense": "Gasto total", "Total Income": "Ingresos totales", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Saldo total", "Expense By Account": "Gasto por cuenta", "Expense By Primary Category": "Gasto por categoría primaria", diff --git a/src/locales/it.json b/src/locales/it.json index eab39932..80d2ca94 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1562,6 +1562,8 @@ "Expense": "Spesa", "Income": "Entrata", "Transfer": "Trasferimento", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Contanti", "Checking Account": "Conto corrente", "Credit Card": "Carta di credito", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Sei sicuro di voler eliminare questo sotto-account?", "Unable to delete this account": "Impossibile eliminare questo account", "Unable to delete this sub-account": "Impossibile eliminare questo sotto-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Transazione", "Transactions": "Transazioni", "Transaction Pictures": "Immagini transazione", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Cerca descrizione transazione", "Unable to retrieve transaction list": "Impossibile recuperare l'elenco delle transazioni", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Intervallo date personalizzato", "Transaction Detail": "Dettaglio transazione", "No transaction data": "Nessun dato di transazione", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Passività totali", "Total Expense": "Spesa totale", "Total Income": "Entrata totale", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Saldo totale", "Expense By Account": "Spesa per conto", "Expense By Primary Category": "Spesa per categoria principale", diff --git a/src/locales/ja.json b/src/locales/ja.json index 3804d429..93dedf90 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1562,6 +1562,8 @@ "Expense": "支出", "Income": "収入", "Transfer": "振替", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "現金", "Checking Account": "当座預金口座", "Credit Card": "クレジットカード", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "この口座を削除できません", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "取引", "Transactions": "取引", "Transaction Pictures": "取引の写真", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "取引の説明を検索", "Unable to retrieve transaction list": "取引リストを取得できません", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "カスタム日付範囲", "Transaction Detail": "取引の詳細", "No transaction data": "取引データがありません", @@ -1823,6 +1827,8 @@ "Total Liabilities": "総負債", "Total Expense": "総支出", "Total Income": "総収入", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "残高合計", "Expense By Account": "口座別の支出", "Expense By Primary Category": "一次カテゴリ別の支出", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index b96b7038..8234cf8c 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1562,6 +1562,8 @@ "Expense": "Despesa", "Income": "Renda", "Transfer": "Transferência", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Dinheiro", "Checking Account": "Conta Corrente", "Credit Card": "Cartão de Crédito", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Tem certeza de que deseja deletar esta subconta?", "Unable to delete this account": "Não foi possível deletar esta conta", "Unable to delete this sub-account": "Não foi possível deletar esta subconta", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Transação", "Transactions": "Transações", "Transaction Pictures": "Imagens das Transações", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Falha ao carregar a imagem, por favor verifique se as configurações \"domain\" e \"root_url\" estão configuradas corretamente.", "Search transaction description": "Pesquisar descrição da transação", "Unable to retrieve transaction list": "Incapaz de recuperar a lista de transações", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Intervalo de Datas Personalizado", "Transaction Detail": "Detalhes da Transação", "No transaction data": "Sem dados de transação", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Total de Passivos", "Total Expense": "Despesa Total", "Total Income": "Renda Total", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Saldo Total", "Expense By Account": "Despesa por Conta", "Expense By Primary Category": "Despesa por Categoria Primária", diff --git a/src/locales/ru.json b/src/locales/ru.json index dfc8be67..29de3a70 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1562,6 +1562,8 @@ "Expense": "Расход", "Income": "Доход", "Transfer": "Перевод", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Наличные", "Checking Account": "Текущий счет", "Credit Card": "Кредитная карта", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Не удалось удалить этот счет", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Транзакция", "Transactions": "Транзакции", "Transaction Pictures": "Изображения транзакций", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Поиск описания транзакции", "Unable to retrieve transaction list": "Не удалось получить список транзакций", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Пользовательский диапазон дат", "Transaction Detail": "Детали транзакции", "No transaction data": "Нет данных о транзакциях", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Общие обязательства", "Total Expense": "Общий расход", "Total Income": "Общий доход", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Общий баланс", "Expense By Account": "Расходы по счетам", "Expense By Primary Category": "Расходы по основной категории", diff --git a/src/locales/uk.json b/src/locales/uk.json index b05d02ba..1be90c25 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1562,6 +1562,8 @@ "Expense": "Витрати", "Income": "Доходи", "Transfer": "Перекази", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Готівка", "Checking Account": "Розрахунковий рахунок", "Credit Card": "Кредитна картка", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Ви впевнені, що хочете видалити цей субрахунок?", "Unable to delete this account": "Не вдалося видалити цей рахунок", "Unable to delete this sub-account": "Не вдалося видалити цей субрахунок", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Транзакція", "Transactions": "Транзакції", "Transaction Pictures": "Зображення транзакцій", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Пошук за описом транзакції", "Unable to retrieve transaction list": "Не вдалося отримати список транзакцій", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Користувацький діапазон дат", "Transaction Detail": "Деталі транзакції", "No transaction data": "Немає даних про транзакції", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Загальні зобов’язання", "Total Expense": "Загальні витрати", "Total Income": "Загальний дохід", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Загальний баланс", "Expense By Account": "Витрати за рахунками", "Expense By Primary Category": "Витрати за основними категоріями", diff --git a/src/locales/vi.json b/src/locales/vi.json index 2b566e57..8d9368f7 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1562,6 +1562,8 @@ "Expense": "Chi phí", "Income": "Thu nhập", "Transfer": "Chuyển khoản", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out", "Cash": "Tiền mặt", "Checking Account": "Tài khoản séc", "Credit Card": "Thẻ tín dụng", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "Are you sure you want to delete this sub-account?", "Unable to delete this account": "Không thể xóa tài khoản này", "Unable to delete this sub-account": "Unable to delete this sub-account", + "Reconciliation Statement": "Reconciliation Statement", "Transaction": "Giao dịch", "Transactions": "Giao dịch", "Transaction Pictures": "Ảnh giao dịch", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.", "Search transaction description": "Tìm kiếm mô tả giao dịch", "Unable to retrieve transaction list": "Không thể lấy danh sách giao dịch", + "Unable to retrieve reconciliation statements": "Unable to retrieve reconciliation statements", "Custom Date Range": "Phạm vi ngày tùy chỉnh", "Transaction Detail": "Chi tiết giao dịch", "No transaction data": "Không có dữ liệu giao dịch", @@ -1823,6 +1827,8 @@ "Total Liabilities": "Tổng nợ phải trả", "Total Expense": "Tổng chi phí", "Total Income": "Tổng thu nhập", + "Total Outflows": "Total Outflows", + "Total Inflows": "Total Inflows", "Total Balance": "Tổng số dư", "Expense By Account": "Chi phí theo tài khoản", "Expense By Primary Category": "Chi phí theo danh mục chính", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index aec10962..d726db96 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1562,6 +1562,8 @@ "Expense": "支出", "Income": "收入", "Transfer": "转账", + "Transfer In": "转入", + "Transfer Out": "转出", "Cash": "现金", "Checking Account": "借记账户", "Credit Card": "信用卡", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "您确定要删除该子账户?", "Unable to delete this account": "无法删除该账户", "Unable to delete this sub-account": "无法删除该子账户", + "Reconciliation Statement": "对账单", "Transaction": "交易", "Transactions": "交易", "Transaction Pictures": "交易图片", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "无法加载图片,请检查配置 \"domain\" 和 \"root_url\" 是否设置正确。", "Search transaction description": "搜索交易描述", "Unable to retrieve transaction list": "无法获取交易列表", + "Unable to retrieve reconciliation statements": "无法获取对账单", "Custom Date Range": "自定义日期范围", "Transaction Detail": "交易详情", "No transaction data": "没有交易数据", @@ -1823,6 +1827,8 @@ "Total Liabilities": "总负债", "Total Expense": "总支出", "Total Income": "总收入", + "Total Outflows": "总流出", + "Total Inflows": "总流入", "Total Balance": "总结余", "Expense By Account": "账户支出", "Expense By Primary Category": "一级分类支出", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index f80a1d6f..b197e2cb 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1562,6 +1562,8 @@ "Expense": "支出", "Income": "收入", "Transfer": "轉帳", + "Transfer In": "轉入", + "Transfer Out": "轉出", "Cash": "現金", "Checking Account": "支票帳戶", "Credit Card": "信用卡", @@ -1627,6 +1629,7 @@ "Are you sure you want to delete this sub-account?": "您確定要刪除這個子帳戶?", "Unable to delete this account": "無法刪除此帳戶", "Unable to delete this sub-account": "無法刪除此子帳戶", + "Reconciliation Statement": "對帳單", "Transaction": "交易", "Transactions": "交易", "Transaction Pictures": "交易圖片", @@ -1804,6 +1807,7 @@ "Failed to load image, please check whether the config \"domain\" and \"root_url\" are set correctly.": "無法載入圖片,請檢查設定 \"domain\" 和 \"root_url\" 是否設定正確。", "Search transaction description": "搜尋交易描述", "Unable to retrieve transaction list": "無法取得交易清單", + "Unable to retrieve reconciliation statements": "無法取得對帳單", "Custom Date Range": "自訂日期範圍", "Transaction Detail": "交易明細", "No transaction data": "沒有交易資料", @@ -1823,6 +1827,8 @@ "Total Liabilities": "總負債", "Total Expense": "總支出", "Total Income": "總收入", + "Total Outflows": "總流出", + "Total Inflows": "總流入", "Total Balance": "總結餘", "Expense By Account": "帳戶支出", "Expense By Primary Category": "一級分類支出", diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 1698f329..15d5c325 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -518,6 +518,12 @@ export interface TransactionListInMonthByPageRequest { readonly keyword: string; } +export interface TransactionReconciliationStatementRequest { + readonly accountId: string; + readonly startTime: number; + readonly endTime: number; +} + export type TransactionGeoLocationResponse = Coordinate; export interface TransactionInfoResponse { @@ -655,6 +661,14 @@ export interface TransactionInfoPageWrapperResponse2 { readonly totalCount: number; } +export interface TransactionReconciliationStatementResponseItem extends TransactionInfoResponse { + readonly accountBalance: number; +} + +export interface TransactionReconciliationStatementResponse { + readonly transactions: TransactionReconciliationStatementResponseItem[]; +} + export interface TransactionPageWrapper { readonly items: Transaction[]; readonly totalCount?: number; diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index 878df3e5..e083a0bf 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -19,6 +19,7 @@ import { type TransactionCreateRequest, type TransactionInfoResponse, type TransactionPageWrapper, + type TransactionReconciliationStatementResponse, Transaction, EMPTY_TRANSACTION_RESULT } from '@/models/transaction.ts'; @@ -961,6 +962,35 @@ export const useTransactionsStore = defineStore('transactions', () => { }); } + function getReconciliationStatements({ accountId, startTime, endTime }: { accountId: string, startTime: number, endTime: number }): Promise { + return new Promise((resolve, reject) => { + services.getReconciliationStatements({ + accountId: accountId, + startTime: startTime, + endTime: endTime + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve reconciliation statements' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to load reconciliation statements', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve reconciliation statements' }); + } else { + reject(error); + } + }); + }); + } + function getTransaction({ transactionId, withPictures }: { transactionId: string, withPictures?: boolean }): Promise { return new Promise((resolve, reject) => { if (!isDefined(withPictures)) { @@ -1328,6 +1358,7 @@ export const useTransactionsStore = defineStore('transactions', () => { getExportTransactionDataRequestByTransactionFilter, loadTransactions, loadMonthlyAllTransactions, + getReconciliationStatements, getTransaction, saveTransaction, deleteTransaction, diff --git a/src/views/base/accounts/AccountListPageBase.ts b/src/views/base/accounts/AccountListPageBase.ts index ed406f82..59b28688 100644 --- a/src/views/base/accounts/AccountListPageBase.ts +++ b/src/views/base/accounts/AccountListPageBase.ts @@ -6,6 +6,7 @@ import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; import { useAccountsStore } from '@/stores/account.ts'; +import type { WeekDayValue } from '@/core/datetime.ts'; import { type AccountCategory, AccountType } from '@/core/account.ts'; import type { Account, CategorizedAccount } from '@/models/account.ts'; @@ -27,6 +28,8 @@ export function useAccountListPageBaseBase() { set: (value) => settingsStore.setShowAccountBalance(value) }); + const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); + const fiscalYearStart = computed(() => userStore.currentUserFiscalYearStart); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); const allAccounts = computed(() => accountsStore.allAccounts); @@ -86,6 +89,8 @@ export function useAccountListPageBaseBase() { displayOrderModified, // computed states showAccountBalance, + firstDayOfWeek, + fiscalYearStart, defaultCurrency, allAccounts, allCategorizedAccountsMap, diff --git a/src/views/base/transactions/ReconciliationStatementPageBase.ts b/src/views/base/transactions/ReconciliationStatementPageBase.ts new file mode 100644 index 00000000..5d8c9847 --- /dev/null +++ b/src/views/base/transactions/ReconciliationStatementPageBase.ts @@ -0,0 +1,168 @@ +import { ref, computed } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import { useSettingsStore } from '@/stores/setting.ts'; +import { useUserStore } from '@/stores/user.ts'; +import { useAccountsStore } from '@/stores/account.ts'; +import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; + +import { TransactionType } from '@/core/transaction.ts'; +import type { Account } from '@/models/account.ts'; +import type { TransactionCategory } from '@/models/transaction_category.ts'; +import type { TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts'; + +import { + getUtcOffsetByUtcOffsetMinutes, + getTimezoneOffsetMinutes, + parseDateFromUnixTime, + getUnixTime +} from '@/lib/datetime.ts'; + +export function useReconciliationStatementPageBase() { + const { + formatUnixTimeToLongDateTime, + formatAmountWithCurrency + } = useI18n(); + + const settingsStore = useSettingsStore(); + const userStore = useUserStore(); + const accountsStore = useAccountsStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + + const accountId = ref(''); + const startTime = ref(0); + const endTime = ref(0); + const reconciliationStatements = ref([]); + + const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); + const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); + + const allAccountsMap = computed>(() => accountsStore.allAccountsMap); + const allCategoriesMap = computed>(() => transactionCategoriesStore.allTransactionCategoriesMap); + + const displayStartDateTime = computed(() => { + return formatUnixTimeToLongDateTime(startTime.value); + }); + + const displayEndDateTime = computed(() => { + return formatUnixTimeToLongDateTime(endTime.value); + }); + + const displayTotalOutflows = computed(() => { + let totalOutflows = 0; + + for (let i = 0; i < reconciliationStatements.value.length; i++) { + const transaction = reconciliationStatements.value[i]; + + if (transaction.type === TransactionType.Expense) { + totalOutflows += transaction.sourceAmount; + } else if (transaction.type === TransactionType.Transfer && transaction.sourceAccountId === accountId.value) { + totalOutflows += transaction.sourceAmount; + } + } + + let currency = defaultCurrency.value; + + if (allAccountsMap.value[accountId.value]) { + currency = allAccountsMap.value[accountId.value].currency; + } + + return formatAmountWithCurrency(totalOutflows, currency); + }); + + const displayTotalInflows = computed(() => { + let totalInflows = 0; + + for (let i = 0; i < reconciliationStatements.value.length; i++) { + const transaction = reconciliationStatements.value[i]; + + if (transaction.type === TransactionType.Income) { + totalInflows += transaction.sourceAmount; + } else if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId === accountId.value) { + totalInflows += transaction.destinationAmount; + } + } + + let currency = defaultCurrency.value; + + if (allAccountsMap.value[accountId.value]) { + currency = allAccountsMap.value[accountId.value].currency; + } + + return formatAmountWithCurrency(totalInflows, currency); + }); + + function getDisplayDateTime(transaction: TransactionReconciliationStatementResponseItem): string { + const transactionTime = getUnixTime(parseDateFromUnixTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value)); + return formatUnixTimeToLongDateTime(transactionTime); + } + + function getDisplayTimezone(transaction: TransactionReconciliationStatementResponseItem): string { + return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`; + } + + function getDisplaySourceAmount(transaction: TransactionReconciliationStatementResponseItem): string { + let currency = defaultCurrency.value; + + if (allAccountsMap.value[transaction.sourceAccountId]) { + currency = allAccountsMap.value[transaction.sourceAccountId].currency; + } + + return formatAmountWithCurrency(transaction.sourceAmount, currency); + } + + function getDisplayDestinationAmount(transaction: TransactionReconciliationStatementResponseItem): string { + let currency = defaultCurrency.value; + + if (allAccountsMap.value[transaction.destinationAccountId]) { + currency = allAccountsMap.value[transaction.destinationAccountId].currency; + } + + return formatAmountWithCurrency(transaction.destinationAmount, currency); + } + + function getDisplayAccountBalance(transaction: TransactionReconciliationStatementResponseItem): string { + let currency = defaultCurrency.value; + let isLiabilityAccount = false; + + if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId === accountId.value) { + if (allAccountsMap.value[transaction.destinationAccountId]) { + currency = allAccountsMap.value[transaction.destinationAccountId].currency; + isLiabilityAccount = allAccountsMap.value[transaction.destinationAccountId].isLiability; + } + } else if (allAccountsMap.value[transaction.sourceAccountId]) { + currency = allAccountsMap.value[transaction.sourceAccountId].currency; + isLiabilityAccount = allAccountsMap.value[transaction.sourceAccountId].isLiability; + } + + if (isLiabilityAccount) { + return formatAmountWithCurrency(-transaction.accountBalance, currency); + } else { + return formatAmountWithCurrency(transaction.accountBalance, currency); + } + } + + return { + // states + accountId, + startTime, + endTime, + reconciliationStatements, + // computed states + currentTimezoneOffsetMinutes, + defaultCurrency, + allAccountsMap, + allCategoriesMap, + displayStartDateTime, + displayEndDateTime, + displayTotalOutflows, + displayTotalInflows, + // functions + getDisplayDateTime, + getDisplayTimezone, + getDisplaySourceAmount, + getDisplayDestinationAmount, + getDisplayAccountBalance + }; +} diff --git a/src/views/desktop/accounts/ListPage.vue b/src/views/desktop/accounts/ListPage.vue index 7e3a2620..aaed3452 100644 --- a/src/views/desktop/accounts/ListPage.vue +++ b/src/views/desktop/accounts/ListPage.vue @@ -211,6 +211,28 @@ :to="`/transaction/list?accountIds=${element.getAccountOrSubAccountId(activeSubAccount[element.id])}`"> {{ tt('Transaction List') }} + + {{ tt('Reconciliation Statement') }} + + + + + + + + + @@ -267,6 +296,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue'; import SnackBar from '@/components/desktop/SnackBar.vue'; import EditDialog from './list/dialogs/EditDialog.vue'; +import ReconciliationStatementDialog from './list/dialogs/ReconciliationStatementDialog.vue'; import AccountFilterSettingsCard from '@/views/desktop/common/cards/AccountFilterSettingsCard.vue'; import { ref, computed, useTemplateRef, watch } from 'vue'; @@ -277,9 +307,13 @@ import { useAccountListPageBaseBase } from '@/views/base/accounts/AccountListPag import { useAccountsStore } from '@/stores/account.ts'; +import { DateRange, DateRangeScene, type LocalizedDateRange, type TimeRangeAndDateType } from '@/core/datetime.ts'; import { AccountType, AccountCategory } from '@/core/account.ts'; import type { Account } from '@/models/account.ts'; +import { isNumber } from '@/lib/common.ts'; +import { getDateRangeByDateType, getDateRangeByBillingCycleDateType } from '@/lib/datetime.ts'; + import { mdiEyeOutline, mdiEyeOffOutline, @@ -290,6 +324,7 @@ import { mdiPencilOutline, mdiDeleteOutline, mdiListBoxOutline, + mdiInvoiceListOutline, mdiDrag, mdiDotsVertical } from '@mdi/js'; @@ -297,16 +332,19 @@ import { type ConfirmDialogType = InstanceType; type SnackBarType = InstanceType; type EditDialogType = InstanceType; +type ReconciliationStatementDialogType = InstanceType; const display = useDisplay(); -const { tt, getCurrencyName, joinMultiText } = useI18n(); +const { tt, getAllDateRanges, getCurrencyName, joinMultiText } = useI18n(); const { loading, showHidden, displayOrderModified, showAccountBalance, + firstDayOfWeek, + fiscalYearStart, allAccounts, allCategorizedAccountsMap, allAccountCount, @@ -322,13 +360,16 @@ const accountsStore = useAccountsStore(); const confirmDialog = useTemplateRef('confirmDialog'); const snackbar = useTemplateRef('snackbar'); const editDialog = useTemplateRef('editDialog'); +const reconciliationStatementDialog = useTemplateRef('reconciliationStatementDialog'); const activeAccountCategoryType = ref(AccountCategory.Default.type); const activeTab = ref('accountPage'); const activeSubAccount = ref>({}); +const accountToShowReconciliationStatement = ref(null); const alwaysShowNav = ref(display.mdAndUp.value); const showNav = ref(display.mdAndUp.value); const showAccountsIncludedInTotalDialog = ref(false); +const showCustomDateRangeDialog = ref(false); const hasAnyVisibleAccount = computed(() => accountsStore.allVisibleAccountsCount > 0); const activeAccountCategory = computed(() => AccountCategory.valueOf(activeAccountCategoryType.value)); @@ -407,6 +448,10 @@ function accountCurrency(account: Account): string | null { } } +function accountReconciliationStatementDateRangs(account: Account): LocalizedDateRange[] { + return getAllDateRanges(DateRangeScene.Normal, true, !!accountsStore.getAccountStatementDate(account.id)); +} + function add(): void { editDialog.value?.open({ category: activeAccountCategoryType.value @@ -440,6 +485,32 @@ function edit(account: Account): void { }); } +function showReconciliationStatementCustomDateRangeDialog(account: Account, dateRangeType?: number): void { + if (!isNumber(dateRangeType) || dateRangeType === DateRange.Custom.type) { + accountToShowReconciliationStatement.value = account; + showCustomDateRangeDialog.value = true; + return; + } + + let dateRange: TimeRangeAndDateType | null = null; + + if (DateRange.isBillingCycle(dateRangeType)) { + dateRange = getDateRangeByBillingCycleDateType(dateRangeType, firstDayOfWeek.value, fiscalYearStart.value, accountsStore.getAccountStatementDate(account.id)); + } else { + dateRange = getDateRangeByDateType(dateRangeType, firstDayOfWeek.value, fiscalYearStart.value); + } + + if (!dateRange) { + return; + } + + reconciliationStatementDialog.value?.open({ + accountId: account.id, + startTime: dateRange.minTime, + endTime: dateRange.maxTime + }); +} + function hide(account: Account, targetAccount: Account, hidden: boolean): void { loading.value = true; @@ -548,6 +619,27 @@ function onMove(event: { moved: { element: { id: string }, oldIndex: number, new }); } +function onCustomDateRangeChanged(minUnixTime: number, maxUnixTime: number): void { + if (!accountToShowReconciliationStatement.value) { + snackbar.value?.showMessage('An error occurred'); + return; + } + + showCustomDateRangeDialog.value = false; + + reconciliationStatementDialog.value?.open({ + accountId: accountToShowReconciliationStatement.value.id, + startTime: minUnixTime, + endTime: maxUnixTime + }); + + accountToShowReconciliationStatement.value = null; +} + +function onShowDateRangeError(message: string): void { + snackbar.value?.showError(message); +} + watch(() => display.mdAndUp.value, (newValue) => { alwaysShowNav.value = newValue; diff --git a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue new file mode 100644 index 00000000..2b3a2072 --- /dev/null +++ b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue @@ -0,0 +1,281 @@ + + +