add reconciliation statement in desktop version

This commit is contained in:
MaysWind
2025-07-21 00:40:02 +08:00
parent 4ba3893b83
commit 515b9af61a
25 changed files with 882 additions and 4 deletions
+1
View File
@@ -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))
+103
View File
@@ -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
+24
View File
@@ -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"`
+22
View File
@@ -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 {
+66
View File
@@ -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 {
@@ -326,7 +326,7 @@ function getTimerPickerItemStyle(textualValue: string, textualCurrentValue: stri
}
}
let angle = -24 * valueDiff;
const angle = -24 * valueDiff;
if (angle > 180) {
return '';
+2 -2
View File
@@ -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) {
+5
View File
@@ -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<ApiResponse<TransactionInfoPageWrapperResponse2>>(`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<TransactionReconciliationStatementResponse> => {
return axios.get<ApiResponse<TransactionReconciliationStatementResponse>>(`v1/transactions/reconciliation_statements.json?account_id=${req.accountId}&start_time=${req.startTime}&end_time=${req.endTime}`);
},
getTransactionStatistics: (req: TransactionStatisticRequest): ApiResponsePromise<TransactionStatisticResponse> => {
const queryParams = [];
+6
View File
@@ -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",
+6
View File
@@ -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",
+6
View File
@@ -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",
+6
View File
@@ -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",
+6
View File
@@ -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": "一次カテゴリ別の支出",
+6
View File
@@ -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",
+6
View File
@@ -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": "Расходы по основной категории",
+6
View File
@@ -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": "Витрати за основними категоріями",
+6
View File
@@ -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",
+6
View File
@@ -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": "一级分类支出",
+6
View File
@@ -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": "一級分類支出",
+14
View File
@@ -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;
+31
View File
@@ -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<TransactionReconciliationStatementResponse> {
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<Transaction> {
return new Promise((resolve, reject) => {
if (!isDefined(withPictures)) {
@@ -1328,6 +1358,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
getExportTransactionDataRequestByTransactionFilter,
loadTransactions,
loadMonthlyAllTransactions,
getReconciliationStatements,
getTransaction,
saveTransaction,
deleteTransaction,
@@ -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<WeekDayValue>(() => userStore.currentUserFirstDayOfWeek);
const fiscalYearStart = computed<number>(() => userStore.currentUserFiscalYearStart);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const allAccounts = computed<Account[]>(() => accountsStore.allAccounts);
@@ -86,6 +89,8 @@ export function useAccountListPageBaseBase() {
displayOrderModified,
// computed states
showAccountBalance,
firstDayOfWeek,
fiscalYearStart,
defaultCurrency,
allAccounts,
allCategorizedAccountsMap,
@@ -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<string>('');
const startTime = ref<number>(0);
const endTime = ref<number>(0);
const reconciliationStatements = ref<TransactionReconciliationStatementResponseItem[]>([]);
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone));
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const allAccountsMap = computed<Record<string, Account>>(() => accountsStore.allAccountsMap);
const allCategoriesMap = computed<Record<string, TransactionCategory>>(() => transactionCategoriesStore.allTransactionCategoriesMap);
const displayStartDateTime = computed<string>(() => {
return formatUnixTimeToLongDateTime(startTime.value);
});
const displayEndDateTime = computed<string>(() => {
return formatUnixTimeToLongDateTime(endTime.value);
});
const displayTotalOutflows = computed<string>(() => {
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<string>(() => {
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
};
}
+93 -1
View File
@@ -211,6 +211,28 @@
:to="`/transaction/list?accountIds=${element.getAccountOrSubAccountId(activeSubAccount[element.id])}`">
{{ tt('Transaction List') }}
</v-btn>
<v-btn class="px-2 ml-1" density="comfortable" color="default" variant="text"
:disabled="loading" :prepend-icon="mdiInvoiceListOutline"
@click="showReconciliationStatementCustomDateRangeDialog(element.getAccountOrSubAccount(activeSubAccount[element.id]))"
v-if="element.type === AccountType.SingleAccount.type || element.getSubAccount(activeSubAccount[element.id])">
{{ tt('Reconciliation Statement') }}
<v-menu activator="parent" :open-on-hover="true">
<v-list>
<template :key="dateRange.type"
v-for="dateRange in accountReconciliationStatementDateRangs(element.getAccountOrSubAccount(activeSubAccount[element.id]))">
<v-list-item class="text-sm" density="compact"
:value="dateRange.type">
<v-list-item-title class="cursor-pointer"
@click="showReconciliationStatementCustomDateRangeDialog(element.getAccountOrSubAccount(activeSubAccount[element.id]), dateRange.type)">
<div class="d-flex align-center">
<span class="text-sm ml-3">{{ dateRange.displayName }}</span>
</div>
</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-menu>
</v-btn>
<v-btn class="px-2 ml-1" density="comfortable" color="default" variant="text"
:class="{ 'd-none': loading, 'hover-display': !loading }"
:disabled="loading"
@@ -258,6 +280,13 @@
</v-dialog>
<edit-dialog ref="editDialog" />
<reconciliation-statement-dialog ref="reconciliationStatementDialog"
@error="onShowDateRangeError" />
<date-range-selection-dialog :title="tt('Custom Date Range')"
v-model:show="showCustomDateRangeDialog"
@dateRange:change="onCustomDateRangeChanged"
@error="onShowDateRangeError" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
@@ -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<typeof ConfirmDialog>;
type SnackBarType = InstanceType<typeof SnackBar>;
type EditDialogType = InstanceType<typeof EditDialog>;
type ReconciliationStatementDialogType = InstanceType<typeof ReconciliationStatementDialog>;
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<ConfirmDialogType>('confirmDialog');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const editDialog = useTemplateRef<EditDialogType>('editDialog');
const reconciliationStatementDialog = useTemplateRef<ReconciliationStatementDialogType>('reconciliationStatementDialog');
const activeAccountCategoryType = ref<number>(AccountCategory.Default.type);
const activeTab = ref<string>('accountPage');
const activeSubAccount = ref<Record<string, string>>({});
const accountToShowReconciliationStatement = ref<Account | null>(null);
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
const showNav = ref<boolean>(display.mdAndUp.value);
const showAccountsIncludedInTotalDialog = ref<boolean>(false);
const showCustomDateRangeDialog = ref<boolean>(false);
const hasAnyVisibleAccount = computed<boolean>(() => accountsStore.allVisibleAccountsCount > 0);
const activeAccountCategory = computed<AccountCategory | undefined>(() => 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;
@@ -0,0 +1,281 @@
<template>
<v-dialog :min-height="loading ? 600 : 400" :persistent="loading" v-model="showState">
<v-card class="pa-6 pa-sm-10 pa-md-12">
<template #title>
<div class="d-flex align-center justify-center">
<div class="d-flex w-100 align-center justify-center">
<h4 class="text-h4">{{ tt('Reconciliation Statement') }}</h4>
<v-progress-circular indeterminate size="22" class="ml-2" v-if="loading"></v-progress-circular>
</div>
</div>
</template>
<template #subtitle>
<div class="text-body-1 text-center text-wrap mt-2">
<span>{{ displayStartDateTime }}</span>
<span> - </span>
<span>{{ displayEndDateTime }}</span>
</div>
</template>
<v-data-table
fixed-header
fixed-footer
multi-sort
density="compact"
item-value="index"
:class="{ 'disabled': loading }"
:headers="dataTableHeaders"
:items="reconciliationStatements"
:no-data-text="loading ? '' : tt('No transaction data')"
v-model:items-per-page="countPerPage"
v-model:page="currentPage"
>
<template #item.time="{ item }">
<span>{{ getDisplayDateTime(item) }}</span>
<v-chip class="ml-1" variant="flat" color="secondary" size="x-small"
v-if="item.utcOffset !== currentTimezoneOffsetMinutes">{{ getDisplayTimezone(item) }}</v-chip>
</template>
<template #item.type="{ item }">
<v-chip label color="secondary" variant="outlined" size="x-small" v-if="item.type === TransactionType.ModifyBalance">{{ tt('Modify Balance') }}</v-chip>
<v-chip label class="text-income" variant="outlined" size="x-small" v-else-if="item.type === TransactionType.Income">{{ tt('Income') }}</v-chip>
<v-chip label class="text-expense" variant="outlined" size="x-small" v-else-if="item.type === TransactionType.Expense">{{ tt('Expense') }}</v-chip>
<v-chip label color="primary" variant="outlined" size="x-small" v-else-if="item.type === TransactionType.Transfer && item.destinationAccountId === accountId">{{ tt('Transfer In') }}</v-chip>
<v-chip label color="primary" variant="outlined" size="x-small" v-else-if="item.type === TransactionType.Transfer && item.sourceAccountId === accountId">{{ tt('Transfer Out') }}</v-chip>
<v-chip label color="primary" variant="outlined" size="x-small" v-else-if="item.type === TransactionType.Transfer">{{ tt('Transfer') }}</v-chip>
<v-chip label color="default" variant="outlined" size="x-small" v-else>{{ tt('Unknown') }}</v-chip>
</template>
<template #item.categoryId="{ item }">
<div class="d-flex align-center">
<span v-if="item.type === TransactionType.ModifyBalance">-</span>
<ItemIcon size="24px" icon-type="category"
:icon-id="allCategoriesMap[item.categoryId].icon"
:color="allCategoriesMap[item.categoryId].color"
v-if="item.type !== TransactionType.ModifyBalance && allCategoriesMap[item.categoryId]"></ItemIcon>
<span class="ml-2" v-if="item.type !== TransactionType.ModifyBalance && allCategoriesMap[item.categoryId]">
{{ allCategoriesMap[item.categoryId].name }}
</span>
</div>
</template>
<template #item.sourceAmount="{ item }">
<span>{{ getDisplaySourceAmount(item) }}</span>
<v-icon class="mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)">{{ getDisplayDestinationAmount(item) }}</span>
</template>
<template #item.sourceAccountId="{ item }">
<div class="d-flex align-center">
<span v-if="item.sourceAccountId && allAccountsMap[item.sourceAccountId]">{{ allAccountsMap[item.sourceAccountId].name }}</span>
<v-icon class="mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.destinationAccountId && allAccountsMap[item.destinationAccountId]">{{ allAccountsMap[item.destinationAccountId].name }}</span>
</div>
</template>
<template #item.accountBalance="{ item }">
<span>{{ getDisplayAccountBalance(item) }}</span>
</template>
<template #bottom>
<div class="title-and-toolbar d-flex align-center text-no-wrap mt-2">
<span class="ml-2">{{ tt('Total Inflows') }}</span>
<span class="text-income" v-if="loading">
<v-skeleton-loader type="text" style="width: 80px" :loading="true"></v-skeleton-loader>
</span>
<span class="text-income ml-2" v-else-if="!loading">
{{ displayTotalInflows }}
</span>
<span class="ml-3">{{ tt('Total Outflows') }}</span>
<span class="text-expense" v-if="loading">
<v-skeleton-loader type="text" style="width: 80px" :loading="true"></v-skeleton-loader>
</span>
<span class="text-expense ml-2" v-else-if="!loading">
{{ displayTotalOutflows }}
</span>
<v-spacer/>
<span v-if="reconciliationStatements && reconciliationStatements.length > 10">
{{ tt('Transactions Per Page') }}
</span>
<v-select class="ml-2" density="compact" max-width="100"
item-title="title"
item-value="value"
:disabled="loading"
:items="reconciliationStatementsTablePageOptions"
v-model="countPerPage"
v-if="reconciliationStatements && reconciliationStatements.length > 10"
/>
<pagination-buttons density="compact"
:disabled="loading"
:totalPageCount="totalPageCount"
v-model="currentPage"
v-if="reconciliationStatements && reconciliationStatements.length > 10">
</pagination-buttons>
</div>
</template>
</v-data-table>
<v-card-text class="overflow-y-visible">
<div class="w-100 d-flex justify-center mt-2 mt-sm-4 mt-md-6 gap-4">
<v-btn color="secondary" variant="tonal"
:disabled="loading" @click="close">{{ tt('Close') }}</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
import { ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useReconciliationStatementPageBase } from '@/views/base/transactions/ReconciliationStatementPageBase.ts';
import { useAccountsStore } from '@/stores/account.ts';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { TransactionType } from '@/core/transaction.ts';
import {
mdiArrowRight
} from '@mdi/js';
interface ReconciliationStatementDialogTablePageOption {
value: number;
title: string;
}
const emit = defineEmits<{
(e: 'error', message: string): void;
}>();
const { tt } = useI18n();
const {
accountId,
startTime,
endTime,
reconciliationStatements,
currentTimezoneOffsetMinutes,
allAccountsMap,
allCategoriesMap,
displayStartDateTime,
displayEndDateTime,
displayTotalOutflows,
displayTotalInflows,
getDisplayDateTime,
getDisplayTimezone,
getDisplaySourceAmount,
getDisplayDestinationAmount,
getDisplayAccountBalance
} = useReconciliationStatementPageBase();
const accountsStore = useAccountsStore();
const transactionCategoriesStore = useTransactionCategoriesStore();
const transactionsStore = useTransactionsStore();
const showState = ref<boolean>(false);
const loading = ref<boolean>(false);
const currentPage = ref<number>(1);
const countPerPage = ref<number>(10);
let rejectFunc: ((reason?: unknown) => void) | null = null;
const account = computed(() => allAccountsMap.value[accountId.value]);
const reconciliationStatementsTablePageOptions = computed<ReconciliationStatementDialogTablePageOption[]>(() => getTablePageOptions(reconciliationStatements.value?.length));
const totalPageCount = computed<number>(() => {
if (!reconciliationStatements.value || reconciliationStatements.value.length < 1) {
return 1;
}
let count = 0;
for (let i = 0; i < reconciliationStatements.value.length; i++) {
count++;
}
return Math.ceil(count / countPerPage.value);
});
const dataTableHeaders = computed<object[]>(() => {
const headers: object[] = [];
const accountBalanceName = account.value?.isLiability ? 'Account Outstanding Balance' : 'Account Balance';
headers.push({ key: 'time', value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true });
headers.push({ key: 'type', value: 'type', title: tt('Type'), sortable: true, nowrap: true });
headers.push({ key: 'categoryId', value: 'categoryId', title: tt('Category'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAccountId', value: 'sourceAccountId', title: tt('Account'), sortable: true, nowrap: true });
headers.push({ key: 'accountBalance', value: 'accountBalance', title: tt(accountBalanceName), sortable: true, nowrap: true });
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
return headers;
});
function getTablePageOptions(linesCount?: number): ReconciliationStatementDialogTablePageOption[] {
const pageOptions: ReconciliationStatementDialogTablePageOption[] = [];
if (!linesCount || linesCount < 1) {
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
}
const availableCountPerPage = [ 5, 10, 15, 20, 25, 30, 50 ];
for (let i = 0; i < availableCountPerPage.length; i++) {
const count = availableCountPerPage[i];
if (linesCount < count) {
break;
}
pageOptions.push({ value: count, title: count.toString() });
}
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
}
function open(options: { accountId: string, startTime: number, endTime: number }): Promise<void> {
accountId.value = options.accountId;
startTime.value = options.startTime;
endTime.value = options.endTime;
reconciliationStatements.value = [];
currentPage.value = 1;
countPerPage.value = 10;
showState.value = true;
loading.value = true;
Promise.all([
accountsStore.loadAllAccounts({ force: false }),
transactionCategoriesStore.loadAllCategories({ force: false })
]).then(() => {
return transactionsStore.getReconciliationStatements({
accountId: options.accountId,
startTime: options.startTime,
endTime: options.endTime
});
}).then(result => {
reconciliationStatements.value = result.transactions;
loading.value = false;
}).catch(error => {
loading.value = false;
if (!error.processed) {
emit('error', error);
showState.value = false;
}
})
return new Promise<void>((resolve, reject) => {
rejectFunc = reject;
});
}
function close(): void {
rejectFunc?.();
showState.value = false;
}
defineExpose({
open
});
</script>